From 8920e0491d6bd6681c2afaf09671befc822a2c14 Mon Sep 17 00:00:00 2001 From: Mares <66750744+mareszhar@users.noreply.github.com> Date: Tue, 26 May 2026 21:12:28 -0400 Subject: [PATCH] Add route handler protocol body helper --- client/packages/admin/src/index.ts | 10 +++ .../__tests__/src/createRouteHandler.test.ts | 64 +++++++++++++++++++ client/packages/core/src/Reactor.js | 12 ++-- .../__types__/routeHandlerProtocolTypeTest.ts | 50 +++++++++++++++ .../packages/core/src/createRouteHandler.ts | 8 ++- client/packages/core/src/index.ts | 12 ++++ .../packages/core/src/routeHandlerProtocol.ts | 46 +++++++++++++ client/packages/react-native/src/index.ts | 10 +++ client/packages/react/src/index.ts | 10 +++ client/packages/react/src/next-ssr/index.tsx | 9 ++- client/packages/solidjs/src/index.ts | 10 +++ client/packages/svelte/src/lib/index.ts | 10 +++ client/packages/vue/src/index.ts | 10 +++ 13 files changed, 253 insertions(+), 8 deletions(-) create mode 100644 client/packages/core/__tests__/src/createRouteHandler.test.ts create mode 100644 client/packages/core/src/__types__/routeHandlerProtocolTypeTest.ts create mode 100644 client/packages/core/src/routeHandlerProtocol.ts diff --git a/client/packages/admin/src/index.ts b/client/packages/admin/src/index.ts index 9ba9037fe0..8b9b50a047 100644 --- a/client/packages/admin/src/index.ts +++ b/client/packages/admin/src/index.ts @@ -73,6 +73,11 @@ import { validateQuery, validateTransactions, createInstantRouteHandler, + createInstantRouteHandlerBody, + type InstantRouteHandlerBody, + type InstantRouteHandlerPayloadByType, + type InstantRouteHandlerRawBody, + type InstantRouteHandlerType, SSEConnection, InstantStream, EventSourceConstructor, @@ -1621,6 +1626,11 @@ export { lookup, i, createInstantRouteHandler, + createInstantRouteHandlerBody, + type InstantRouteHandlerBody, + type InstantRouteHandlerPayloadByType, + type InstantRouteHandlerRawBody, + type InstantRouteHandlerType, Webhooks, WebhooksManager, type WebhookAction, diff --git a/client/packages/core/__tests__/src/createRouteHandler.test.ts b/client/packages/core/__tests__/src/createRouteHandler.test.ts new file mode 100644 index 0000000000..5a18c6f12a --- /dev/null +++ b/client/packages/core/__tests__/src/createRouteHandler.test.ts @@ -0,0 +1,64 @@ +import { expect, test } from 'vitest'; +import { + createInstantRouteHandler, + createInstantRouteHandlerBody, + type User, +} from '../../src'; + +const user: User = { + id: 'user-id', + refresh_token: 'refresh-token', + isGuest: false, +}; + +test('createInstantRouteHandlerBody creates a typed sync-user body', () => { + expect( + createInstantRouteHandlerBody('sync-user', { + appId: 'app-id', + user, + }), + ).toEqual({ + type: 'sync-user', + appId: 'app-id', + user, + }); +}); + +test('createInstantRouteHandler accepts a sync-user body from the shared helper', async () => { + const handler = createInstantRouteHandler({ appId: 'app-id' }); + const response = await handler.POST( + new Request('https://example.com/api/instant', { + method: 'POST', + body: JSON.stringify( + createInstantRouteHandlerBody('sync-user', { + appId: 'app-id', + user, + }), + ), + }), + ); + + expect(response.status).toBe(200); + expect(await response.json()).toEqual({ ok: true }); + expect(response.headers.get('set-cookie')).toContain('instant_user_app-id='); + expect(response.headers.get('set-cookie')).toContain('Max-Age=604800'); +}); + +test('createInstantRouteHandler keeps rejecting unknown route handler types', async () => { + const handler = createInstantRouteHandler({ appId: 'app-id' }); + const response = await handler.POST( + new Request('https://example.com/api/instant', { + method: 'POST', + body: JSON.stringify({ + type: 'future-type', + appId: 'app-id', + }), + }), + ); + + expect(response.status).toBe(400); + expect(await response.json()).toEqual({ + ok: false, + error: 'Unknown type: future-type', + }); +}); diff --git a/client/packages/core/src/Reactor.js b/client/packages/core/src/Reactor.js index b4bb36ed60..9c6a3cc40b 100644 --- a/client/packages/core/src/Reactor.js +++ b/client/packages/core/src/Reactor.js @@ -33,6 +33,7 @@ import { validate as validateUUID } from 'uuid'; import { WSConnection, SSEConnection } from './Connection.ts'; import { SyncTable } from './SyncTable.ts'; import { InstantStream } from './Stream.ts'; +import { createInstantRouteHandlerBody } from './routeHandlerProtocol.ts'; /** @typedef {import('./utils/log.ts').Logger} Logger */ /** @typedef {import('./Connection.ts').Connection} Connection */ @@ -2202,11 +2203,12 @@ export default class Reactor { try { await fetch(this.config.firstPartyPath + '/', { method: 'POST', - body: JSON.stringify({ - type: 'sync-user', - appId: this.config.appId, - user: user, - }), + body: JSON.stringify( + createInstantRouteHandlerBody('sync-user', { + appId: this.config.appId, + user, + }), + ), headers: { 'Content-Type': 'application/json', }, diff --git a/client/packages/core/src/__types__/routeHandlerProtocolTypeTest.ts b/client/packages/core/src/__types__/routeHandlerProtocolTypeTest.ts new file mode 100644 index 0000000000..d6832b0ac6 --- /dev/null +++ b/client/packages/core/src/__types__/routeHandlerProtocolTypeTest.ts @@ -0,0 +1,50 @@ +import { createInstantRouteHandlerBody } from '../index.ts'; +import type { + InstantRouteHandlerBody, + InstantRouteHandlerPayloadByType, + InstantRouteHandlerRawBody, + InstantRouteHandlerType, + User, +} from '../index.ts'; +import type { Equal, Expect, NotAny } from './typeUtils.ts'; + +const user: User = { + id: 'user-id', + refresh_token: 'refresh-token', + isGuest: false, +}; + +const syncUserBody = createInstantRouteHandlerBody('sync-user', { + appId: 'app-id', + user, +}); + +type _routeHandlerProtocolCases = [ + Expect>, + Expect>>, + Expect>, + Expect< + Equal< + InstantRouteHandlerPayloadByType['sync-user'], + { appId: string; user: User | null } + > + >, +]; + +const rawBody: InstantRouteHandlerRawBody = { + type: 'future-type', + appId: 'app-id', + user: { anything: true }, + extra: true, +}; + +rawBody; + +// @ts-expect-error unknown route handler types should not be constructible. +createInstantRouteHandlerBody('unknown-type', { appId: 'app-id', user }); + +createInstantRouteHandlerBody('sync-user', { + appId: 'app-id', + // @ts-expect-error sync-user bodies use the official User shape. + user: { refresh_token: 'refresh-token' }, +}); diff --git a/client/packages/core/src/createRouteHandler.ts b/client/packages/core/src/createRouteHandler.ts index 59d8942664..87ce706e62 100644 --- a/client/packages/core/src/createRouteHandler.ts +++ b/client/packages/core/src/createRouteHandler.ts @@ -1,4 +1,5 @@ import type { User } from './clientTypes.js'; +import type { InstantRouteHandlerRawBody } from './routeHandlerProtocol.ts'; type CreateRouteHandlerConfig = { appId: string; @@ -37,7 +38,7 @@ function errorResponse(status: number, message: string) { export const createInstantRouteHandler = (config: CreateRouteHandlerConfig) => { return { POST: async (req: Request) => { - let body: { type?: string; appId?: string; user?: User | null }; + let body: InstantRouteHandlerRawBody; try { body = await req.json(); } catch { @@ -54,7 +55,10 @@ export const createInstantRouteHandler = (config: CreateRouteHandlerConfig) => { switch (body.type) { case 'sync-user': - return createUserSyncResponse(config, body.user ?? null); + return createUserSyncResponse( + config, + (body.user as User | null | undefined) ?? null, + ); default: return errorResponse(400, `Unknown type: ${body.type}`); } diff --git a/client/packages/core/src/index.ts b/client/packages/core/src/index.ts index e48d79d477..907cf2174c 100644 --- a/client/packages/core/src/index.ts +++ b/client/packages/core/src/index.ts @@ -28,6 +28,13 @@ import { type StoreInterfaceStoreName, } from './utils/PersistedObject.ts'; import { createInstantRouteHandler } from './createRouteHandler.ts'; +import { + createInstantRouteHandlerBody, + type InstantRouteHandlerBody, + type InstantRouteHandlerPayloadByType, + type InstantRouteHandlerRawBody, + type InstantRouteHandlerType, +} from './routeHandlerProtocol.ts'; import { parseSchemaFromJSON } from './parseSchemaFromJSON.ts'; import type { @@ -1225,6 +1232,11 @@ export { type StoreInterfaceClass, type StoreInterfaceStoreName, createInstantRouteHandler, + createInstantRouteHandlerBody, + type InstantRouteHandlerBody, + type InstantRouteHandlerPayloadByType, + type InstantRouteHandlerRawBody, + type InstantRouteHandlerType, }; /** @deprecated Use StoreInterface instead */ diff --git a/client/packages/core/src/routeHandlerProtocol.ts b/client/packages/core/src/routeHandlerProtocol.ts new file mode 100644 index 0000000000..0415ba387d --- /dev/null +++ b/client/packages/core/src/routeHandlerProtocol.ts @@ -0,0 +1,46 @@ +import type { User } from './clientTypes.ts'; + +/** + * Known payloads sent to `firstPartyPath` and handled by + * `createInstantRouteHandler`. + */ +export type InstantRouteHandlerPayloadByType = { + 'sync-user': { + appId: string; + user: User | null; + }; +}; + +/** Known `type` values for Instant route handler request bodies. */ +export type InstantRouteHandlerType = keyof InstantRouteHandlerPayloadByType; + +/** A valid request body for Instant's first-party route handler protocol. */ +export type InstantRouteHandlerBody< + Type extends InstantRouteHandlerType = InstantRouteHandlerType, +> = { + [KnownType in Type]: { + type: KnownType; + } & InstantRouteHandlerPayloadByType[KnownType]; +}[Type]; + +/** + * An untrusted request body before route handler validation. + * + * Use `InstantRouteHandlerBody` after checking the `type` and payload shape. + */ +export type InstantRouteHandlerRawBody = { + type?: InstantRouteHandlerType | (string & {}); + appId?: string; + user?: unknown; + [key: string]: unknown; +}; + +/** Creates a typed request body for Instant's first-party route handler. */ +export function createInstantRouteHandlerBody< + Type extends InstantRouteHandlerType, +>( + type: Type, + payload: InstantRouteHandlerPayloadByType[Type], +): InstantRouteHandlerBody { + return { type, ...payload } as InstantRouteHandlerBody; +} diff --git a/client/packages/react-native/src/index.ts b/client/packages/react-native/src/index.ts index 35a0eb76d0..94268e9b12 100644 --- a/client/packages/react-native/src/index.ts +++ b/client/packages/react-native/src/index.ts @@ -22,6 +22,11 @@ import { // types createInstantRouteHandler, + createInstantRouteHandlerBody, + type InstantRouteHandlerBody, + type InstantRouteHandlerPayloadByType, + type InstantRouteHandlerRawBody, + type InstantRouteHandlerType, type RoomSchemaShape, type InstantQuery, type InstantQueryResult, @@ -190,6 +195,11 @@ export { lookup, i, createInstantRouteHandler, + createInstantRouteHandlerBody, + type InstantRouteHandlerBody, + type InstantRouteHandlerPayloadByType, + type InstantRouteHandlerRawBody, + type InstantRouteHandlerType, InstantReactNativeDatabase, // error diff --git a/client/packages/react/src/index.ts b/client/packages/react/src/index.ts index b92020412a..8febbc74b9 100644 --- a/client/packages/react/src/index.ts +++ b/client/packages/react/src/index.ts @@ -92,6 +92,11 @@ import { type SyncTableSetupError, StoreInterface, createInstantRouteHandler, + createInstantRouteHandlerBody, + type InstantRouteHandlerBody, + type InstantRouteHandlerPayloadByType, + type InstantRouteHandlerRawBody, + type InstantRouteHandlerType, type StoreInterfaceStoreName, InstantWritableStream, InstantReadableStream, @@ -212,4 +217,9 @@ export { // Server helper createInstantRouteHandler, + createInstantRouteHandlerBody, + type InstantRouteHandlerBody, + type InstantRouteHandlerPayloadByType, + type InstantRouteHandlerRawBody, + type InstantRouteHandlerType, }; diff --git a/client/packages/react/src/next-ssr/index.tsx b/client/packages/react/src/next-ssr/index.tsx index e080972e6f..a828c26a28 100644 --- a/client/packages/react/src/next-ssr/index.tsx +++ b/client/packages/react/src/next-ssr/index.tsx @@ -16,7 +16,14 @@ export { export { InstantNextDatabase } from './InstantNextDatabase.tsx'; export { InstantSuspenseProvider } from './InstantSuspenseProvider.tsx'; -export { createInstantRouteHandler } from '@instantdb/core'; +export { + createInstantRouteHandler, + createInstantRouteHandlerBody, + type InstantRouteHandlerBody, + type InstantRouteHandlerPayloadByType, + type InstantRouteHandlerRawBody, + type InstantRouteHandlerType, +} from '@instantdb/core'; /** * diff --git a/client/packages/solidjs/src/index.ts b/client/packages/solidjs/src/index.ts index 69beef09d8..4e5128f998 100644 --- a/client/packages/solidjs/src/index.ts +++ b/client/packages/solidjs/src/index.ts @@ -87,6 +87,11 @@ import { type SyncTableSetupError, StoreInterface, createInstantRouteHandler, + createInstantRouteHandlerBody, + type InstantRouteHandlerBody, + type InstantRouteHandlerPayloadByType, + type InstantRouteHandlerRawBody, + type InstantRouteHandlerType, type StoreInterfaceStoreName, } from '@instantdb/core'; @@ -191,4 +196,9 @@ export { // Server helper createInstantRouteHandler, + createInstantRouteHandlerBody, + type InstantRouteHandlerBody, + type InstantRouteHandlerPayloadByType, + type InstantRouteHandlerRawBody, + type InstantRouteHandlerType, }; diff --git a/client/packages/svelte/src/lib/index.ts b/client/packages/svelte/src/lib/index.ts index 92b32bb239..bb3e3777cd 100644 --- a/client/packages/svelte/src/lib/index.ts +++ b/client/packages/svelte/src/lib/index.ts @@ -87,6 +87,11 @@ import { type SyncTableSetupError, StoreInterface, createInstantRouteHandler, + createInstantRouteHandlerBody, + type InstantRouteHandlerBody, + type InstantRouteHandlerPayloadByType, + type InstantRouteHandlerRawBody, + type InstantRouteHandlerType, type StoreInterfaceStoreName, } from '@instantdb/core'; @@ -197,4 +202,9 @@ export { // Server helper createInstantRouteHandler, + createInstantRouteHandlerBody, + type InstantRouteHandlerBody, + type InstantRouteHandlerPayloadByType, + type InstantRouteHandlerRawBody, + type InstantRouteHandlerType, }; diff --git a/client/packages/vue/src/index.ts b/client/packages/vue/src/index.ts index 1fd2588066..3cb74c84cc 100644 --- a/client/packages/vue/src/index.ts +++ b/client/packages/vue/src/index.ts @@ -87,6 +87,11 @@ import { type SyncTableSetupError, StoreInterface, createInstantRouteHandler, + createInstantRouteHandlerBody, + type InstantRouteHandlerBody, + type InstantRouteHandlerPayloadByType, + type InstantRouteHandlerRawBody, + type InstantRouteHandlerType, type StoreInterfaceStoreName, } from '@instantdb/core'; @@ -196,4 +201,9 @@ export { // Server helper createInstantRouteHandler, + createInstantRouteHandlerBody, + type InstantRouteHandlerBody, + type InstantRouteHandlerPayloadByType, + type InstantRouteHandlerRawBody, + type InstantRouteHandlerType, };