@synap-core/sdk 1.1.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -28,17 +28,26 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
28
28
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
29
 
30
30
  // src/index.ts
31
- var index_exports = {};
32
- __export(index_exports, {
33
- createSynapClient: () => createSynapClient
31
+ var src_exports = {};
32
+ __export(src_exports, {
33
+ createSynapClient: () => createSynapClient,
34
+ ensureTemplate: () => ensureTemplate,
35
+ ensuredWorkspaceId: () => ensuredWorkspaceId,
36
+ parseEnsureTemplateResult: () => parseEnsureTemplateResult
34
37
  });
35
- module.exports = __toCommonJS(index_exports);
38
+ module.exports = __toCommonJS(src_exports);
36
39
 
37
40
  // src/client.ts
38
41
  var import_client = require("@trpc/client");
39
42
  var import_superjson = __toESM(require("superjson"), 1);
43
+ var HUB_KEY_PREFIXES = ["synap_hub_", "synap_user_"];
40
44
  function createSynapClient(options) {
41
45
  const { podUrl, apiKey, workspaceId, extraHeaders, onAuthError } = options;
46
+ if (apiKey && HUB_KEY_PREFIXES.some((p) => apiKey.startsWith(p))) {
47
+ throw new Error(
48
+ "createSynapClient: this is a hub/user API key; /trpc only accepts a session or the local-mode token. Use @synap-core/hub-rest-client against /api/hub/* instead."
49
+ );
50
+ }
42
51
  const url = `${podUrl.replace(/\/$/, "")}/trpc`;
43
52
  const baseFetch = (input, init) => fetch(input, { ...init, credentials: "include" });
44
53
  const wrappedFetch = onAuthError ? async (input, init) => {
@@ -99,7 +108,7 @@ function createSynapClient(options) {
99
108
  headers: () => ({
100
109
  // Session token mode (Kratos, preferred for non-browser clients)
101
110
  ...options.sessionToken ? { "x-session-token": options.sessionToken } : {},
102
- // API key mode (Bearer, for legacy SDK usage)
111
+ // LOCAL_MODE token (Bearer); hub/user API keys are rejected above
103
112
  ...apiKey && !options.sessionToken ? { Authorization: `Bearer ${apiKey}` } : {},
104
113
  ...workspaceId ? { "x-workspace-id": workspaceId } : {},
105
114
  ...extraHeaders
@@ -108,8 +117,41 @@ function createSynapClient(options) {
108
117
  ]
109
118
  });
110
119
  }
120
+
121
+ // src/templates.ts
122
+ var STATUSES = /* @__PURE__ */ new Set([
123
+ "installed",
124
+ "missing",
125
+ "proposed",
126
+ "installing",
127
+ "exists",
128
+ "retired",
129
+ "not_in_catalog",
130
+ "unknown"
131
+ ]);
132
+ function parseEnsureTemplateResult(raw) {
133
+ const status = raw && typeof raw === "object" ? raw.status : void 0;
134
+ if (typeof status !== "string" || !STATUSES.has(status)) {
135
+ throw new Error(
136
+ `ensureTemplate: the pod answered with an unknown status (${String(status)}).`
137
+ );
138
+ }
139
+ return raw;
140
+ }
141
+ async function ensureTemplate(client, slug, opts = {}) {
142
+ const input = { slug };
143
+ if (opts.install) input.install = true;
144
+ if (opts.reason) input.reason = opts.reason;
145
+ return parseEnsureTemplateResult(await client.workspaces.ensureTemplate.mutate(input));
146
+ }
147
+ function ensuredWorkspaceId(result) {
148
+ return result.status === "installed" || result.status === "installing" ? result.workspaceId : null;
149
+ }
111
150
  // Annotate the CommonJS export names for ESM import in node:
112
151
  0 && (module.exports = {
113
- createSynapClient
152
+ createSynapClient,
153
+ ensureTemplate,
154
+ ensuredWorkspaceId,
155
+ parseEnsureTemplateResult
114
156
  });
115
157
  //# sourceMappingURL=index.cjs.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../src/client.ts"],"sourcesContent":["export { createSynapClient } from \"./client\";\nexport type { SynapClient, SynapClientOptions, AppRouter } from \"./client\";\n","/**\n * @synap-core/sdk — Type-safe tRPC client for Synap data pods.\n *\n * Usage:\n * ```typescript\n * import { createSynapClient } from \"@synap-core/sdk\";\n *\n * const synap = createSynapClient({\n * podUrl: \"https://pod.example.com\",\n * apiKey: \"sk_live_...\",\n * workspaceId: \"ws_...\",\n * });\n *\n * // Fully typed — autocomplete works on all 47+ routers\n * const entities = await synap.entities.list.query({ limit: 20 });\n * const workspace = await synap.workspaces.get.query();\n * ```\n */\n\nimport {\n createTRPCClient,\n httpBatchLink,\n type CreateTRPCClient,\n} from \"@trpc/client\";\nimport superjson from \"superjson\";\nimport type { AppRouter } from \"@synap-core/api-types\";\n\nexport interface SynapClientOptions {\n /**\n * Base URL of the data pod (no trailing slash, no `/trpc` suffix).\n * @example \"https://pod.example.com\"\n */\n podUrl: string;\n\n /**\n * Ory Kratos session token (for example, from the Pod's\n * `/api/federation/exchange` endpoint).\n * Sent as `X-Session-Token` header — preferred for non-browser clients\n * like extensions, CLI tools, and mobile apps.\n * @example \"oathkeeper_token_...\"\n */\n sessionToken?: string;\n\n /**\n * API key for authentication (Bearer token).\n * Generate one in your workspace developer settings.\n * If omitted, the client falls back to cookie-based Kratos session auth.\n * @example \"sk_live_abc123...\"\n */\n apiKey?: string;\n\n /**\n * Workspace ID to scope all requests to.\n * Pass empty string for procedures that don't require workspace context\n * (e.g. workspaces.list, which is protectedProcedure only).\n * @example \"ws_01jk...\"\n */\n workspaceId: string;\n\n /**\n * Additional headers to include in every request.\n */\n extraHeaders?: Record<string, string>;\n\n /**\n * Called when the pod returns a `401 Unauthorized` (expired/invalid session\n * or API key). The app should sign out and re-authenticate (re-handshake).\n *\n * The pod's auth middleware can reply with a plain-JSON body\n * (`{\"error\":\"Invalid session\"}`) that is NOT in tRPC batch format and would\n * otherwise crash the superjson transformer; when an `onAuthError` handler is\n * provided the client rewrites such responses into a well-formed tRPC error so\n * the query rejects gracefully instead of throwing a parse error.\n */\n onAuthError?: (error: { status: number; message: string }) => void;\n}\n\n/**\n * Creates a fully-typed tRPC client connected to a Synap data pod.\n *\n * All 47+ tRPC routers are available with full TypeScript autocomplete\n * and compile-time safety. Auth is handled via Bearer token + workspace header.\n */\nexport function createSynapClient(\n options: SynapClientOptions,\n): CreateTRPCClient<AppRouter> {\n const { podUrl, apiKey, workspaceId, extraHeaders, onAuthError } = options;\n\n const url = `${podUrl.replace(/\\/$/, \"\")}/trpc`;\n\n // Base fetch: always include credentials so session cookies are sent in\n // cookie-auth mode. When apiKey is present, the Bearer token takes precedence\n // on the pod.\n const baseFetch: typeof fetch = (input, init) =>\n fetch(input, { ...init, credentials: \"include\" });\n\n // When an onAuthError handler is provided, wrap fetch to catch 401 (and other\n // non-tRPC error) responses. The pod's auth middleware replies with a plain\n // JSON body that is not a tRPC batch array; left untouched it crashes the\n // superjson transformer. We notify the handler and rewrite the body into a\n // valid tRPC error envelope so the query rejects cleanly.\n const wrappedFetch: typeof fetch = onAuthError\n ? async (input, init) => {\n const response = await baseFetch(input, init);\n if (response.ok) return response;\n\n const cloned = response.clone();\n let body = \"\";\n try {\n body = await cloned.text();\n } catch {\n body = \"\";\n }\n\n let isTrpcFormat = false;\n try {\n isTrpcFormat = Array.isArray(JSON.parse(body));\n } catch {\n isTrpcFormat = false;\n }\n if (isTrpcFormat) return response;\n\n const parsedMessage = (() => {\n try {\n const parsed = JSON.parse(body) as {\n error?: string;\n message?: string;\n };\n return parsed.error ?? parsed.message ?? null;\n } catch {\n return null;\n }\n })();\n // Never forward a server-controlled string as user-facing copy — map by\n // status. The raw server detail is preserved (untrusted) on `data.detail`.\n const message =\n response.status === 401\n ? \"Session expired. Please sign in again.\"\n : `Request failed (${response.status}).`;\n\n if (response.status === 401) {\n onAuthError({ status: 401, message });\n }\n\n const trpcError = [\n {\n error: {\n message,\n code: response.status === 401 ? -32001 : -32000,\n data: {\n code:\n response.status === 401\n ? \"UNAUTHORIZED\"\n : \"INTERNAL_SERVER_ERROR\",\n httpStatus: response.status,\n // Server-supplied detail — untrusted; consumers must escape it\n // before rendering (never inject as HTML).\n detail: parsedMessage ?? undefined,\n },\n\n },\n },\n ];\n return new Response(JSON.stringify(trpcError), {\n status: response.status,\n headers: { \"content-type\": \"application/json\" },\n });\n }\n : baseFetch;\n\n return createTRPCClient<AppRouter>({\n links: [\n httpBatchLink({\n url,\n transformer: superjson,\n fetch: wrappedFetch,\n headers: () => ({\n // Session token mode (Kratos, preferred for non-browser clients)\n ...(options.sessionToken ? { \"x-session-token\": options.sessionToken } : {}),\n // API key mode (Bearer, for legacy SDK usage)\n ...(apiKey && !options.sessionToken ? { Authorization: `Bearer ${apiKey}` } : {}),\n ...(workspaceId ? { \"x-workspace-id\": workspaceId } : {}),\n ...extraHeaders,\n }),\n }),\n ],\n });\n}\n\nexport type SynapClient = ReturnType<typeof createSynapClient>;\n\n// Re-export type helpers so consumers don't need @trpc/server\nexport type { AppRouter };\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACmBA,oBAIO;AACP,uBAAsB;AA2Df,SAAS,kBACd,SAC6B;AAC7B,QAAM,EAAE,QAAQ,QAAQ,aAAa,cAAc,YAAY,IAAI;AAEnE,QAAM,MAAM,GAAG,OAAO,QAAQ,OAAO,EAAE,CAAC;AAKxC,QAAM,YAA0B,CAAC,OAAO,SACtC,MAAM,OAAO,EAAE,GAAG,MAAM,aAAa,UAAU,CAAC;AAOlD,QAAM,eAA6B,cAC/B,OAAO,OAAO,SAAS;AACrB,UAAM,WAAW,MAAM,UAAU,OAAO,IAAI;AAC5C,QAAI,SAAS,GAAI,QAAO;AAExB,UAAM,SAAS,SAAS,MAAM;AAC9B,QAAI,OAAO;AACX,QAAI;AACF,aAAO,MAAM,OAAO,KAAK;AAAA,IAC3B,QAAQ;AACN,aAAO;AAAA,IACT;AAEA,QAAI,eAAe;AACnB,QAAI;AACF,qBAAe,MAAM,QAAQ,KAAK,MAAM,IAAI,CAAC;AAAA,IAC/C,QAAQ;AACN,qBAAe;AAAA,IACjB;AACA,QAAI,aAAc,QAAO;AAEzB,UAAM,iBAAiB,MAAM;AAC3B,UAAI;AACF,cAAM,SAAS,KAAK,MAAM,IAAI;AAI9B,eAAO,OAAO,SAAS,OAAO,WAAW;AAAA,MAC3C,QAAQ;AACN,eAAO;AAAA,MACT;AAAA,IACF,GAAG;AAGH,UAAM,UACJ,SAAS,WAAW,MAChB,2CACA,mBAAmB,SAAS,MAAM;AAExC,QAAI,SAAS,WAAW,KAAK;AAC3B,kBAAY,EAAE,QAAQ,KAAK,QAAQ,CAAC;AAAA,IACtC;AAEA,UAAM,YAAY;AAAA,MAChB;AAAA,QACE,OAAO;AAAA,UACL;AAAA,UACA,MAAM,SAAS,WAAW,MAAM,SAAS;AAAA,UACzC,MAAM;AAAA,YACJ,MACE,SAAS,WAAW,MAChB,iBACA;AAAA,YACN,YAAY,SAAS;AAAA;AAAA;AAAA,YAGrB,QAAQ,iBAAiB;AAAA,UAC3B;AAAA,QAEF;AAAA,MACF;AAAA,IACF;AACA,WAAO,IAAI,SAAS,KAAK,UAAU,SAAS,GAAG;AAAA,MAC7C,QAAQ,SAAS;AAAA,MACjB,SAAS,EAAE,gBAAgB,mBAAmB;AAAA,IAChD,CAAC;AAAA,EACH,IACA;AAEJ,aAAO,gCAA4B;AAAA,IACjC,OAAO;AAAA,UACL,6BAAc;AAAA,QACZ;AAAA,QACA,aAAa,iBAAAA;AAAA,QACb,OAAO;AAAA,QACP,SAAS,OAAO;AAAA;AAAA,UAEd,GAAI,QAAQ,eAAe,EAAE,mBAAmB,QAAQ,aAAa,IAAI,CAAC;AAAA;AAAA,UAE1E,GAAI,UAAU,CAAC,QAAQ,eAAe,EAAE,eAAe,UAAU,MAAM,GAAG,IAAI,CAAC;AAAA,UAC/E,GAAI,cAAc,EAAE,kBAAkB,YAAY,IAAI,CAAC;AAAA,UACvD,GAAG;AAAA,QACL;AAAA,MACF,CAAC;AAAA,IACH;AAAA,EACF,CAAC;AACH;","names":["superjson"]}
1
+ {"version":3,"sources":["../src/index.ts","../src/client.ts","../src/templates.ts"],"sourcesContent":["export { createSynapClient } from \"./client\";\nexport type { SynapClient, SynapClientOptions, AppRouter } from \"./client\";\nexport {\n ensureTemplate,\n ensuredWorkspaceId,\n parseEnsureTemplateResult,\n} from \"./templates\";\nexport type {\n EnsureTemplateClient,\n EnsureTemplateInput,\n EnsureTemplateMissingTemplate,\n EnsureTemplateOptions,\n EnsureTemplateResult,\n EnsureTemplateStatus,\n} from \"./templates\";\n","/**\n * @synap-core/sdk — Type-safe tRPC client for Synap data pods.\n *\n * Usage:\n * ```typescript\n * import { createSynapClient } from \"@synap-core/sdk\";\n *\n * const synap = createSynapClient({\n * podUrl: \"https://pod.example.com\",\n * sessionToken: \"<Kratos session token>\",\n * workspaceId: \"ws_...\",\n * });\n *\n * // Fully typed — autocomplete works on all 47+ routers\n * const entities = await synap.entities.list.query({ limit: 20 });\n * const workspace = await synap.workspaces.get.query();\n * ```\n */\n\nimport {\n createTRPCClient,\n httpBatchLink,\n type CreateTRPCClient,\n} from \"@trpc/client\";\nimport superjson from \"superjson\";\nimport type { AppRouter } from \"@synap-core/api-types\";\n\nexport interface SynapClientOptions {\n /**\n * Base URL of the data pod (no trailing slash, no `/trpc` suffix).\n * @example \"https://pod.example.com\"\n */\n podUrl: string;\n\n /**\n * Ory Kratos session token (the `session_token` a Kratos API / native\n * login flow returns).\n * Sent as `X-Session-Token` header — preferred for non-browser clients\n * like extensions, CLI tools, and mobile apps.\n * @example \"oathkeeper_token_...\"\n */\n sessionToken?: string;\n\n /**\n * LOCAL_MODE token only (the desktop host's `LOCAL_AUTH_TOKEN`), sent as\n * `Authorization: Bearer`. `/trpc` sits behind Ory session auth, so a real\n * pod refuses hub / user API keys (`synap_hub_*`, `synap_user_*`) with a 401;\n * this client throws early if given one. Use `@synap-core/hub-rest-client`\n * against `/api/hub/*` for hub / user API keys, or `sessionToken` / cookies\n * for a signed-in user.\n * If omitted, the client falls back to cookie-based Kratos session auth.\n */\n apiKey?: string;\n\n /**\n * Workspace ID to scope all requests to.\n * Pass empty string for procedures that don't require workspace context\n * (e.g. workspaces.list, which is protectedProcedure only).\n * @example \"ws_01jk...\"\n */\n workspaceId: string;\n\n /**\n * Additional headers to include in every request.\n */\n extraHeaders?: Record<string, string>;\n\n /**\n * Called when the pod returns a `401 Unauthorized` (expired/invalid session\n * or API key). The app should sign out and re-authenticate (re-handshake).\n *\n * The pod's auth middleware can reply with a plain-JSON body\n * (`{\"error\":\"Invalid session\"}`) that is NOT in tRPC batch format and would\n * otherwise crash the superjson transformer; when an `onAuthError` handler is\n * provided the client rewrites such responses into a well-formed tRPC error so\n * the query rejects gracefully instead of throwing a parse error.\n */\n onAuthError?: (error: { status: number; message: string }) => void;\n}\n\n/**\n * Prefixes of hub / user API keys (SSOT: `KEY_PREFIXES` in synap-backend\n * `packages/database/src/schema/api-keys.ts`; hub_inbound keys use `synap_hub_`).\n * The LOCAL_MODE token is random bytes and never starts with these.\n */\nconst HUB_KEY_PREFIXES = [\"synap_hub_\", \"synap_user_\"] as const;\n\n/**\n * Creates a fully-typed tRPC client connected to a Synap data pod.\n *\n * All 47+ tRPC routers are available with full TypeScript autocomplete\n * and compile-time safety. Auth is handled via Bearer token + workspace header.\n */\nexport function createSynapClient(\n options: SynapClientOptions,\n): CreateTRPCClient<AppRouter> {\n const { podUrl, apiKey, workspaceId, extraHeaders, onAuthError } = options;\n\n if (apiKey && HUB_KEY_PREFIXES.some((p) => apiKey.startsWith(p))) {\n throw new Error(\n \"createSynapClient: this is a hub/user API key; /trpc only accepts a session or the local-mode token. \" +\n \"Use @synap-core/hub-rest-client against /api/hub/* instead.\",\n );\n }\n\n const url = `${podUrl.replace(/\\/$/, \"\")}/trpc`;\n\n // Base fetch: always include credentials so session cookies are sent in\n // cookie-auth mode. When apiKey is present, the Bearer token takes precedence\n // on the pod.\n const baseFetch: typeof fetch = (input, init) =>\n fetch(input, { ...init, credentials: \"include\" });\n\n // When an onAuthError handler is provided, wrap fetch to catch 401 (and other\n // non-tRPC error) responses. The pod's auth middleware replies with a plain\n // JSON body that is not a tRPC batch array; left untouched it crashes the\n // superjson transformer. We notify the handler and rewrite the body into a\n // valid tRPC error envelope so the query rejects cleanly.\n const wrappedFetch: typeof fetch = onAuthError\n ? async (input, init) => {\n const response = await baseFetch(input, init);\n if (response.ok) return response;\n\n const cloned = response.clone();\n let body = \"\";\n try {\n body = await cloned.text();\n } catch {\n body = \"\";\n }\n\n let isTrpcFormat = false;\n try {\n isTrpcFormat = Array.isArray(JSON.parse(body));\n } catch {\n isTrpcFormat = false;\n }\n if (isTrpcFormat) return response;\n\n const parsedMessage = (() => {\n try {\n const parsed = JSON.parse(body) as {\n error?: string;\n message?: string;\n };\n return parsed.error ?? parsed.message ?? null;\n } catch {\n return null;\n }\n })();\n // Never forward a server-controlled string as user-facing copy — map by\n // status. The raw server detail is preserved (untrusted) on `data.detail`.\n const message =\n response.status === 401\n ? \"Session expired. Please sign in again.\"\n : `Request failed (${response.status}).`;\n\n if (response.status === 401) {\n onAuthError({ status: 401, message });\n }\n\n const trpcError = [\n {\n error: {\n message,\n code: response.status === 401 ? -32001 : -32000,\n data: {\n code:\n response.status === 401\n ? \"UNAUTHORIZED\"\n : \"INTERNAL_SERVER_ERROR\",\n httpStatus: response.status,\n // Server-supplied detail — untrusted; consumers must escape it\n // before rendering (never inject as HTML).\n detail: parsedMessage ?? undefined,\n },\n\n },\n },\n ];\n return new Response(JSON.stringify(trpcError), {\n status: response.status,\n headers: { \"content-type\": \"application/json\" },\n });\n }\n : baseFetch;\n\n return createTRPCClient<AppRouter>({\n links: [\n httpBatchLink({\n url,\n transformer: superjson,\n fetch: wrappedFetch,\n headers: () => ({\n // Session token mode (Kratos, preferred for non-browser clients)\n ...(options.sessionToken ? { \"x-session-token\": options.sessionToken } : {}),\n // LOCAL_MODE token (Bearer); hub/user API keys are rejected above\n ...(apiKey && !options.sessionToken ? { Authorization: `Bearer ${apiKey}` } : {}),\n ...(workspaceId ? { \"x-workspace-id\": workspaceId } : {}),\n ...extraHeaders,\n }),\n }),\n ],\n });\n}\n\nexport type SynapClient = ReturnType<typeof createSynapClient>;\n\n// Re-export type helpers so consumers don't need @trpc/server\nexport type { AppRouter };\n","/**\n * ensureTemplate — an app asks the pod whether the template it runs on is\n * installed, and (optionally) asks for it to be installed.\n *\n * TEMPLATE-LIFECYCLE Contract B. ONE door: tRPC `workspaces.ensureTemplate`\n * (Hub REST mirror `POST /api/hub/workspaces/ensure-template`). The pod\n * resolves the definition from the catalog and installs through its governed\n * install door — an app NEVER sends a definition. \"Installed\" means an ACTIVE\n * workspace whose `settings.packageSlug` is the slug.\n *\n * The result union below is HAND-WRITTEN from the contract\n * (`TEMPLATE-LIFECYCLE/DESIGN.md`, Contract B) because the published\n * `@synap-core/api-types` does not carry the procedure until the next regen.\n * Once it does, `EnsureTemplateResult` should become\n * `RouterOutputs[\"workspaces\"][\"ensureTemplate\"]`.\n *\n * A failed call REJECTS. It is never folded into `missing`: \"the pod said the\n * template is not installed\" and \"we could not ask the pod\" are different\n * facts, and only the first may offer an install.\n */\n\nexport interface EnsureTemplateInput {\n slug: string;\n /** Ask the pod to install it when missing. */\n install?: boolean;\n /** One plain sentence: why the app needs it (shown on the install request). */\n reason?: string;\n}\n\nexport interface EnsureTemplateMissingTemplate {\n slug: string;\n name: string;\n description: string | null;\n icon: string | null;\n}\n\nexport type EnsureTemplateResult =\n /** Installed: an ACTIVE workspace carries the template. */\n | { status: \"installed\"; workspaceId: string; version: string | null; drifted: boolean }\n /** Not installed, and no install was asked. */\n | { status: \"missing\"; template: EnsureTemplateMissingTemplate }\n /** Install asked by an agent or app key: the governed install filed a proposal. */\n | { status: \"proposed\"; proposalId: string; reviewUrl: string }\n /** Install asked by a person: it is being installed now. */\n | { status: \"installing\"; workspaceId: string }\n /**\n * Another live space already holds this template's domain (one space per\n * domain), so the pod will not install a twin. The person decides what to\n * do with that space; `guidance` is the pod's own sentence.\n */\n | { status: \"exists\"; workspaceId: string; workspaceName: string; guidance: string }\n /** The catalog retired the template; `replacedBy` is its successor, if any. */\n | { status: \"retired\"; replacedBy: string | null }\n /** A COMPLETE catalog sync does not list the slug. */\n | { status: \"not_in_catalog\" }\n /** The pod cannot tell yet (catalog cache cold or last sync incomplete). */\n | { status: \"unknown\" };\n\nexport type EnsureTemplateStatus = EnsureTemplateResult[\"status\"];\n\n/**\n * The slice of the tRPC client this helper needs. Structural on purpose: it\n * accepts `createSynapClient(...)` and a React `trpc.useUtils().client` alike,\n * and keeps compiling before the procedure lands in the generated types.\n */\nexport interface EnsureTemplateClient {\n workspaces: {\n ensureTemplate: {\n mutate(input: EnsureTemplateInput): Promise<unknown>;\n };\n };\n}\n\nexport interface EnsureTemplateOptions {\n install?: boolean;\n reason?: string;\n}\n\nconst STATUSES: ReadonlySet<string> = new Set<EnsureTemplateStatus>([\n \"installed\",\n \"missing\",\n \"proposed\",\n \"installing\",\n \"exists\",\n \"retired\",\n \"not_in_catalog\",\n \"unknown\",\n]);\n\n/**\n * Narrow the wire answer. An answer whose `status` is not one the contract\n * names is a protocol error and throws: guessing \"missing\" would offer an\n * install the pod never said was needed.\n */\nexport function parseEnsureTemplateResult(raw: unknown): EnsureTemplateResult {\n const status =\n raw && typeof raw === \"object\" ? (raw as { status?: unknown }).status : undefined;\n if (typeof status !== \"string\" || !STATUSES.has(status)) {\n throw new Error(\n `ensureTemplate: the pod answered with an unknown status (${String(status)}).`,\n );\n }\n return raw as EnsureTemplateResult;\n}\n\nexport async function ensureTemplate(\n client: EnsureTemplateClient,\n slug: string,\n opts: EnsureTemplateOptions = {},\n): Promise<EnsureTemplateResult> {\n const input: EnsureTemplateInput = { slug };\n if (opts.install) input.install = true;\n if (opts.reason) input.reason = opts.reason;\n return parseEnsureTemplateResult(await client.workspaces.ensureTemplate.mutate(input));\n}\n\n/** The workspace the template lives in, when the answer names one. */\nexport function ensuredWorkspaceId(result: EnsureTemplateResult): string | null {\n return result.status === \"installed\" || result.status === \"installing\"\n ? result.workspaceId\n : null;\n}\n"],"mappings":"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","names":["superjson"]}
package/dist/index.d.cts CHANGED
@@ -11,7 +11,7 @@ export { AppRouter } from '@synap-core/api-types';
11
11
  *
12
12
  * const synap = createSynapClient({
13
13
  * podUrl: "https://pod.example.com",
14
- * apiKey: "sk_live_...",
14
+ * sessionToken: "<Kratos session token>",
15
15
  * workspaceId: "ws_...",
16
16
  * });
17
17
  *
@@ -28,18 +28,21 @@ interface SynapClientOptions {
28
28
  */
29
29
  podUrl: string;
30
30
  /**
31
- * Ory Kratos session token (for example, from the Pod's
32
- * `/api/federation/exchange` endpoint).
31
+ * Ory Kratos session token (the `session_token` a Kratos API / native
32
+ * login flow returns).
33
33
  * Sent as `X-Session-Token` header — preferred for non-browser clients
34
34
  * like extensions, CLI tools, and mobile apps.
35
35
  * @example "oathkeeper_token_..."
36
36
  */
37
37
  sessionToken?: string;
38
38
  /**
39
- * API key for authentication (Bearer token).
40
- * Generate one in your workspace developer settings.
39
+ * LOCAL_MODE token only (the desktop host's `LOCAL_AUTH_TOKEN`), sent as
40
+ * `Authorization: Bearer`. `/trpc` sits behind Ory session auth, so a real
41
+ * pod refuses hub / user API keys (`synap_hub_*`, `synap_user_*`) with a 401;
42
+ * this client throws early if given one. Use `@synap-core/hub-rest-client`
43
+ * against `/api/hub/*` for hub / user API keys, or `sessionToken` / cookies
44
+ * for a signed-in user.
41
45
  * If omitted, the client falls back to cookie-based Kratos session auth.
42
- * @example "sk_live_abc123..."
43
46
  */
44
47
  apiKey?: string;
45
48
  /**
@@ -77,4 +80,112 @@ interface SynapClientOptions {
77
80
  declare function createSynapClient(options: SynapClientOptions): CreateTRPCClient<AppRouter>;
78
81
  type SynapClient = ReturnType<typeof createSynapClient>;
79
82
 
80
- export { type SynapClient, type SynapClientOptions, createSynapClient };
83
+ /**
84
+ * ensureTemplate — an app asks the pod whether the template it runs on is
85
+ * installed, and (optionally) asks for it to be installed.
86
+ *
87
+ * TEMPLATE-LIFECYCLE Contract B. ONE door: tRPC `workspaces.ensureTemplate`
88
+ * (Hub REST mirror `POST /api/hub/workspaces/ensure-template`). The pod
89
+ * resolves the definition from the catalog and installs through its governed
90
+ * install door — an app NEVER sends a definition. "Installed" means an ACTIVE
91
+ * workspace whose `settings.packageSlug` is the slug.
92
+ *
93
+ * The result union below is HAND-WRITTEN from the contract
94
+ * (`TEMPLATE-LIFECYCLE/DESIGN.md`, Contract B) because the published
95
+ * `@synap-core/api-types` does not carry the procedure until the next regen.
96
+ * Once it does, `EnsureTemplateResult` should become
97
+ * `RouterOutputs["workspaces"]["ensureTemplate"]`.
98
+ *
99
+ * A failed call REJECTS. It is never folded into `missing`: "the pod said the
100
+ * template is not installed" and "we could not ask the pod" are different
101
+ * facts, and only the first may offer an install.
102
+ */
103
+ interface EnsureTemplateInput {
104
+ slug: string;
105
+ /** Ask the pod to install it when missing. */
106
+ install?: boolean;
107
+ /** One plain sentence: why the app needs it (shown on the install request). */
108
+ reason?: string;
109
+ }
110
+ interface EnsureTemplateMissingTemplate {
111
+ slug: string;
112
+ name: string;
113
+ description: string | null;
114
+ icon: string | null;
115
+ }
116
+ type EnsureTemplateResult =
117
+ /** Installed: an ACTIVE workspace carries the template. */
118
+ {
119
+ status: "installed";
120
+ workspaceId: string;
121
+ version: string | null;
122
+ drifted: boolean;
123
+ }
124
+ /** Not installed, and no install was asked. */
125
+ | {
126
+ status: "missing";
127
+ template: EnsureTemplateMissingTemplate;
128
+ }
129
+ /** Install asked by an agent or app key: the governed install filed a proposal. */
130
+ | {
131
+ status: "proposed";
132
+ proposalId: string;
133
+ reviewUrl: string;
134
+ }
135
+ /** Install asked by a person: it is being installed now. */
136
+ | {
137
+ status: "installing";
138
+ workspaceId: string;
139
+ }
140
+ /**
141
+ * Another live space already holds this template's domain (one space per
142
+ * domain), so the pod will not install a twin. The person decides what to
143
+ * do with that space; `guidance` is the pod's own sentence.
144
+ */
145
+ | {
146
+ status: "exists";
147
+ workspaceId: string;
148
+ workspaceName: string;
149
+ guidance: string;
150
+ }
151
+ /** The catalog retired the template; `replacedBy` is its successor, if any. */
152
+ | {
153
+ status: "retired";
154
+ replacedBy: string | null;
155
+ }
156
+ /** A COMPLETE catalog sync does not list the slug. */
157
+ | {
158
+ status: "not_in_catalog";
159
+ }
160
+ /** The pod cannot tell yet (catalog cache cold or last sync incomplete). */
161
+ | {
162
+ status: "unknown";
163
+ };
164
+ type EnsureTemplateStatus = EnsureTemplateResult["status"];
165
+ /**
166
+ * The slice of the tRPC client this helper needs. Structural on purpose: it
167
+ * accepts `createSynapClient(...)` and a React `trpc.useUtils().client` alike,
168
+ * and keeps compiling before the procedure lands in the generated types.
169
+ */
170
+ interface EnsureTemplateClient {
171
+ workspaces: {
172
+ ensureTemplate: {
173
+ mutate(input: EnsureTemplateInput): Promise<unknown>;
174
+ };
175
+ };
176
+ }
177
+ interface EnsureTemplateOptions {
178
+ install?: boolean;
179
+ reason?: string;
180
+ }
181
+ /**
182
+ * Narrow the wire answer. An answer whose `status` is not one the contract
183
+ * names is a protocol error and throws: guessing "missing" would offer an
184
+ * install the pod never said was needed.
185
+ */
186
+ declare function parseEnsureTemplateResult(raw: unknown): EnsureTemplateResult;
187
+ declare function ensureTemplate(client: EnsureTemplateClient, slug: string, opts?: EnsureTemplateOptions): Promise<EnsureTemplateResult>;
188
+ /** The workspace the template lives in, when the answer names one. */
189
+ declare function ensuredWorkspaceId(result: EnsureTemplateResult): string | null;
190
+
191
+ export { type EnsureTemplateClient, type EnsureTemplateInput, type EnsureTemplateMissingTemplate, type EnsureTemplateOptions, type EnsureTemplateResult, type EnsureTemplateStatus, type SynapClient, type SynapClientOptions, createSynapClient, ensureTemplate, ensuredWorkspaceId, parseEnsureTemplateResult };
package/dist/index.d.ts CHANGED
@@ -11,7 +11,7 @@ export { AppRouter } from '@synap-core/api-types';
11
11
  *
12
12
  * const synap = createSynapClient({
13
13
  * podUrl: "https://pod.example.com",
14
- * apiKey: "sk_live_...",
14
+ * sessionToken: "<Kratos session token>",
15
15
  * workspaceId: "ws_...",
16
16
  * });
17
17
  *
@@ -28,18 +28,21 @@ interface SynapClientOptions {
28
28
  */
29
29
  podUrl: string;
30
30
  /**
31
- * Ory Kratos session token (for example, from the Pod's
32
- * `/api/federation/exchange` endpoint).
31
+ * Ory Kratos session token (the `session_token` a Kratos API / native
32
+ * login flow returns).
33
33
  * Sent as `X-Session-Token` header — preferred for non-browser clients
34
34
  * like extensions, CLI tools, and mobile apps.
35
35
  * @example "oathkeeper_token_..."
36
36
  */
37
37
  sessionToken?: string;
38
38
  /**
39
- * API key for authentication (Bearer token).
40
- * Generate one in your workspace developer settings.
39
+ * LOCAL_MODE token only (the desktop host's `LOCAL_AUTH_TOKEN`), sent as
40
+ * `Authorization: Bearer`. `/trpc` sits behind Ory session auth, so a real
41
+ * pod refuses hub / user API keys (`synap_hub_*`, `synap_user_*`) with a 401;
42
+ * this client throws early if given one. Use `@synap-core/hub-rest-client`
43
+ * against `/api/hub/*` for hub / user API keys, or `sessionToken` / cookies
44
+ * for a signed-in user.
41
45
  * If omitted, the client falls back to cookie-based Kratos session auth.
42
- * @example "sk_live_abc123..."
43
46
  */
44
47
  apiKey?: string;
45
48
  /**
@@ -77,4 +80,112 @@ interface SynapClientOptions {
77
80
  declare function createSynapClient(options: SynapClientOptions): CreateTRPCClient<AppRouter>;
78
81
  type SynapClient = ReturnType<typeof createSynapClient>;
79
82
 
80
- export { type SynapClient, type SynapClientOptions, createSynapClient };
83
+ /**
84
+ * ensureTemplate — an app asks the pod whether the template it runs on is
85
+ * installed, and (optionally) asks for it to be installed.
86
+ *
87
+ * TEMPLATE-LIFECYCLE Contract B. ONE door: tRPC `workspaces.ensureTemplate`
88
+ * (Hub REST mirror `POST /api/hub/workspaces/ensure-template`). The pod
89
+ * resolves the definition from the catalog and installs through its governed
90
+ * install door — an app NEVER sends a definition. "Installed" means an ACTIVE
91
+ * workspace whose `settings.packageSlug` is the slug.
92
+ *
93
+ * The result union below is HAND-WRITTEN from the contract
94
+ * (`TEMPLATE-LIFECYCLE/DESIGN.md`, Contract B) because the published
95
+ * `@synap-core/api-types` does not carry the procedure until the next regen.
96
+ * Once it does, `EnsureTemplateResult` should become
97
+ * `RouterOutputs["workspaces"]["ensureTemplate"]`.
98
+ *
99
+ * A failed call REJECTS. It is never folded into `missing`: "the pod said the
100
+ * template is not installed" and "we could not ask the pod" are different
101
+ * facts, and only the first may offer an install.
102
+ */
103
+ interface EnsureTemplateInput {
104
+ slug: string;
105
+ /** Ask the pod to install it when missing. */
106
+ install?: boolean;
107
+ /** One plain sentence: why the app needs it (shown on the install request). */
108
+ reason?: string;
109
+ }
110
+ interface EnsureTemplateMissingTemplate {
111
+ slug: string;
112
+ name: string;
113
+ description: string | null;
114
+ icon: string | null;
115
+ }
116
+ type EnsureTemplateResult =
117
+ /** Installed: an ACTIVE workspace carries the template. */
118
+ {
119
+ status: "installed";
120
+ workspaceId: string;
121
+ version: string | null;
122
+ drifted: boolean;
123
+ }
124
+ /** Not installed, and no install was asked. */
125
+ | {
126
+ status: "missing";
127
+ template: EnsureTemplateMissingTemplate;
128
+ }
129
+ /** Install asked by an agent or app key: the governed install filed a proposal. */
130
+ | {
131
+ status: "proposed";
132
+ proposalId: string;
133
+ reviewUrl: string;
134
+ }
135
+ /** Install asked by a person: it is being installed now. */
136
+ | {
137
+ status: "installing";
138
+ workspaceId: string;
139
+ }
140
+ /**
141
+ * Another live space already holds this template's domain (one space per
142
+ * domain), so the pod will not install a twin. The person decides what to
143
+ * do with that space; `guidance` is the pod's own sentence.
144
+ */
145
+ | {
146
+ status: "exists";
147
+ workspaceId: string;
148
+ workspaceName: string;
149
+ guidance: string;
150
+ }
151
+ /** The catalog retired the template; `replacedBy` is its successor, if any. */
152
+ | {
153
+ status: "retired";
154
+ replacedBy: string | null;
155
+ }
156
+ /** A COMPLETE catalog sync does not list the slug. */
157
+ | {
158
+ status: "not_in_catalog";
159
+ }
160
+ /** The pod cannot tell yet (catalog cache cold or last sync incomplete). */
161
+ | {
162
+ status: "unknown";
163
+ };
164
+ type EnsureTemplateStatus = EnsureTemplateResult["status"];
165
+ /**
166
+ * The slice of the tRPC client this helper needs. Structural on purpose: it
167
+ * accepts `createSynapClient(...)` and a React `trpc.useUtils().client` alike,
168
+ * and keeps compiling before the procedure lands in the generated types.
169
+ */
170
+ interface EnsureTemplateClient {
171
+ workspaces: {
172
+ ensureTemplate: {
173
+ mutate(input: EnsureTemplateInput): Promise<unknown>;
174
+ };
175
+ };
176
+ }
177
+ interface EnsureTemplateOptions {
178
+ install?: boolean;
179
+ reason?: string;
180
+ }
181
+ /**
182
+ * Narrow the wire answer. An answer whose `status` is not one the contract
183
+ * names is a protocol error and throws: guessing "missing" would offer an
184
+ * install the pod never said was needed.
185
+ */
186
+ declare function parseEnsureTemplateResult(raw: unknown): EnsureTemplateResult;
187
+ declare function ensureTemplate(client: EnsureTemplateClient, slug: string, opts?: EnsureTemplateOptions): Promise<EnsureTemplateResult>;
188
+ /** The workspace the template lives in, when the answer names one. */
189
+ declare function ensuredWorkspaceId(result: EnsureTemplateResult): string | null;
190
+
191
+ export { type EnsureTemplateClient, type EnsureTemplateInput, type EnsureTemplateMissingTemplate, type EnsureTemplateOptions, type EnsureTemplateResult, type EnsureTemplateStatus, type SynapClient, type SynapClientOptions, createSynapClient, ensureTemplate, ensuredWorkspaceId, parseEnsureTemplateResult };
package/dist/index.js CHANGED
@@ -4,8 +4,14 @@ import {
4
4
  httpBatchLink
5
5
  } from "@trpc/client";
6
6
  import superjson from "superjson";
7
+ var HUB_KEY_PREFIXES = ["synap_hub_", "synap_user_"];
7
8
  function createSynapClient(options) {
8
9
  const { podUrl, apiKey, workspaceId, extraHeaders, onAuthError } = options;
10
+ if (apiKey && HUB_KEY_PREFIXES.some((p) => apiKey.startsWith(p))) {
11
+ throw new Error(
12
+ "createSynapClient: this is a hub/user API key; /trpc only accepts a session or the local-mode token. Use @synap-core/hub-rest-client against /api/hub/* instead."
13
+ );
14
+ }
9
15
  const url = `${podUrl.replace(/\/$/, "")}/trpc`;
10
16
  const baseFetch = (input, init) => fetch(input, { ...init, credentials: "include" });
11
17
  const wrappedFetch = onAuthError ? async (input, init) => {
@@ -66,7 +72,7 @@ function createSynapClient(options) {
66
72
  headers: () => ({
67
73
  // Session token mode (Kratos, preferred for non-browser clients)
68
74
  ...options.sessionToken ? { "x-session-token": options.sessionToken } : {},
69
- // API key mode (Bearer, for legacy SDK usage)
75
+ // LOCAL_MODE token (Bearer); hub/user API keys are rejected above
70
76
  ...apiKey && !options.sessionToken ? { Authorization: `Bearer ${apiKey}` } : {},
71
77
  ...workspaceId ? { "x-workspace-id": workspaceId } : {},
72
78
  ...extraHeaders
@@ -75,7 +81,40 @@ function createSynapClient(options) {
75
81
  ]
76
82
  });
77
83
  }
84
+
85
+ // src/templates.ts
86
+ var STATUSES = /* @__PURE__ */ new Set([
87
+ "installed",
88
+ "missing",
89
+ "proposed",
90
+ "installing",
91
+ "exists",
92
+ "retired",
93
+ "not_in_catalog",
94
+ "unknown"
95
+ ]);
96
+ function parseEnsureTemplateResult(raw) {
97
+ const status = raw && typeof raw === "object" ? raw.status : void 0;
98
+ if (typeof status !== "string" || !STATUSES.has(status)) {
99
+ throw new Error(
100
+ `ensureTemplate: the pod answered with an unknown status (${String(status)}).`
101
+ );
102
+ }
103
+ return raw;
104
+ }
105
+ async function ensureTemplate(client, slug, opts = {}) {
106
+ const input = { slug };
107
+ if (opts.install) input.install = true;
108
+ if (opts.reason) input.reason = opts.reason;
109
+ return parseEnsureTemplateResult(await client.workspaces.ensureTemplate.mutate(input));
110
+ }
111
+ function ensuredWorkspaceId(result) {
112
+ return result.status === "installed" || result.status === "installing" ? result.workspaceId : null;
113
+ }
78
114
  export {
79
- createSynapClient
115
+ createSynapClient,
116
+ ensureTemplate,
117
+ ensuredWorkspaceId,
118
+ parseEnsureTemplateResult
80
119
  };
81
120
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/client.ts"],"sourcesContent":["/**\n * @synap-core/sdk — Type-safe tRPC client for Synap data pods.\n *\n * Usage:\n * ```typescript\n * import { createSynapClient } from \"@synap-core/sdk\";\n *\n * const synap = createSynapClient({\n * podUrl: \"https://pod.example.com\",\n * apiKey: \"sk_live_...\",\n * workspaceId: \"ws_...\",\n * });\n *\n * // Fully typed — autocomplete works on all 47+ routers\n * const entities = await synap.entities.list.query({ limit: 20 });\n * const workspace = await synap.workspaces.get.query();\n * ```\n */\n\nimport {\n createTRPCClient,\n httpBatchLink,\n type CreateTRPCClient,\n} from \"@trpc/client\";\nimport superjson from \"superjson\";\nimport type { AppRouter } from \"@synap-core/api-types\";\n\nexport interface SynapClientOptions {\n /**\n * Base URL of the data pod (no trailing slash, no `/trpc` suffix).\n * @example \"https://pod.example.com\"\n */\n podUrl: string;\n\n /**\n * Ory Kratos session token (for example, from the Pod's\n * `/api/federation/exchange` endpoint).\n * Sent as `X-Session-Token` header — preferred for non-browser clients\n * like extensions, CLI tools, and mobile apps.\n * @example \"oathkeeper_token_...\"\n */\n sessionToken?: string;\n\n /**\n * API key for authentication (Bearer token).\n * Generate one in your workspace developer settings.\n * If omitted, the client falls back to cookie-based Kratos session auth.\n * @example \"sk_live_abc123...\"\n */\n apiKey?: string;\n\n /**\n * Workspace ID to scope all requests to.\n * Pass empty string for procedures that don't require workspace context\n * (e.g. workspaces.list, which is protectedProcedure only).\n * @example \"ws_01jk...\"\n */\n workspaceId: string;\n\n /**\n * Additional headers to include in every request.\n */\n extraHeaders?: Record<string, string>;\n\n /**\n * Called when the pod returns a `401 Unauthorized` (expired/invalid session\n * or API key). The app should sign out and re-authenticate (re-handshake).\n *\n * The pod's auth middleware can reply with a plain-JSON body\n * (`{\"error\":\"Invalid session\"}`) that is NOT in tRPC batch format and would\n * otherwise crash the superjson transformer; when an `onAuthError` handler is\n * provided the client rewrites such responses into a well-formed tRPC error so\n * the query rejects gracefully instead of throwing a parse error.\n */\n onAuthError?: (error: { status: number; message: string }) => void;\n}\n\n/**\n * Creates a fully-typed tRPC client connected to a Synap data pod.\n *\n * All 47+ tRPC routers are available with full TypeScript autocomplete\n * and compile-time safety. Auth is handled via Bearer token + workspace header.\n */\nexport function createSynapClient(\n options: SynapClientOptions,\n): CreateTRPCClient<AppRouter> {\n const { podUrl, apiKey, workspaceId, extraHeaders, onAuthError } = options;\n\n const url = `${podUrl.replace(/\\/$/, \"\")}/trpc`;\n\n // Base fetch: always include credentials so session cookies are sent in\n // cookie-auth mode. When apiKey is present, the Bearer token takes precedence\n // on the pod.\n const baseFetch: typeof fetch = (input, init) =>\n fetch(input, { ...init, credentials: \"include\" });\n\n // When an onAuthError handler is provided, wrap fetch to catch 401 (and other\n // non-tRPC error) responses. The pod's auth middleware replies with a plain\n // JSON body that is not a tRPC batch array; left untouched it crashes the\n // superjson transformer. We notify the handler and rewrite the body into a\n // valid tRPC error envelope so the query rejects cleanly.\n const wrappedFetch: typeof fetch = onAuthError\n ? async (input, init) => {\n const response = await baseFetch(input, init);\n if (response.ok) return response;\n\n const cloned = response.clone();\n let body = \"\";\n try {\n body = await cloned.text();\n } catch {\n body = \"\";\n }\n\n let isTrpcFormat = false;\n try {\n isTrpcFormat = Array.isArray(JSON.parse(body));\n } catch {\n isTrpcFormat = false;\n }\n if (isTrpcFormat) return response;\n\n const parsedMessage = (() => {\n try {\n const parsed = JSON.parse(body) as {\n error?: string;\n message?: string;\n };\n return parsed.error ?? parsed.message ?? null;\n } catch {\n return null;\n }\n })();\n // Never forward a server-controlled string as user-facing copy — map by\n // status. The raw server detail is preserved (untrusted) on `data.detail`.\n const message =\n response.status === 401\n ? \"Session expired. Please sign in again.\"\n : `Request failed (${response.status}).`;\n\n if (response.status === 401) {\n onAuthError({ status: 401, message });\n }\n\n const trpcError = [\n {\n error: {\n message,\n code: response.status === 401 ? -32001 : -32000,\n data: {\n code:\n response.status === 401\n ? \"UNAUTHORIZED\"\n : \"INTERNAL_SERVER_ERROR\",\n httpStatus: response.status,\n // Server-supplied detail — untrusted; consumers must escape it\n // before rendering (never inject as HTML).\n detail: parsedMessage ?? undefined,\n },\n\n },\n },\n ];\n return new Response(JSON.stringify(trpcError), {\n status: response.status,\n headers: { \"content-type\": \"application/json\" },\n });\n }\n : baseFetch;\n\n return createTRPCClient<AppRouter>({\n links: [\n httpBatchLink({\n url,\n transformer: superjson,\n fetch: wrappedFetch,\n headers: () => ({\n // Session token mode (Kratos, preferred for non-browser clients)\n ...(options.sessionToken ? { \"x-session-token\": options.sessionToken } : {}),\n // API key mode (Bearer, for legacy SDK usage)\n ...(apiKey && !options.sessionToken ? { Authorization: `Bearer ${apiKey}` } : {}),\n ...(workspaceId ? { \"x-workspace-id\": workspaceId } : {}),\n ...extraHeaders,\n }),\n }),\n ],\n });\n}\n\nexport type SynapClient = ReturnType<typeof createSynapClient>;\n\n// Re-export type helpers so consumers don't need @trpc/server\nexport type { AppRouter };\n"],"mappings":";AAmBA;AAAA,EACE;AAAA,EACA;AAAA,OAEK;AACP,OAAO,eAAe;AA2Df,SAAS,kBACd,SAC6B;AAC7B,QAAM,EAAE,QAAQ,QAAQ,aAAa,cAAc,YAAY,IAAI;AAEnE,QAAM,MAAM,GAAG,OAAO,QAAQ,OAAO,EAAE,CAAC;AAKxC,QAAM,YAA0B,CAAC,OAAO,SACtC,MAAM,OAAO,EAAE,GAAG,MAAM,aAAa,UAAU,CAAC;AAOlD,QAAM,eAA6B,cAC/B,OAAO,OAAO,SAAS;AACrB,UAAM,WAAW,MAAM,UAAU,OAAO,IAAI;AAC5C,QAAI,SAAS,GAAI,QAAO;AAExB,UAAM,SAAS,SAAS,MAAM;AAC9B,QAAI,OAAO;AACX,QAAI;AACF,aAAO,MAAM,OAAO,KAAK;AAAA,IAC3B,QAAQ;AACN,aAAO;AAAA,IACT;AAEA,QAAI,eAAe;AACnB,QAAI;AACF,qBAAe,MAAM,QAAQ,KAAK,MAAM,IAAI,CAAC;AAAA,IAC/C,QAAQ;AACN,qBAAe;AAAA,IACjB;AACA,QAAI,aAAc,QAAO;AAEzB,UAAM,iBAAiB,MAAM;AAC3B,UAAI;AACF,cAAM,SAAS,KAAK,MAAM,IAAI;AAI9B,eAAO,OAAO,SAAS,OAAO,WAAW;AAAA,MAC3C,QAAQ;AACN,eAAO;AAAA,MACT;AAAA,IACF,GAAG;AAGH,UAAM,UACJ,SAAS,WAAW,MAChB,2CACA,mBAAmB,SAAS,MAAM;AAExC,QAAI,SAAS,WAAW,KAAK;AAC3B,kBAAY,EAAE,QAAQ,KAAK,QAAQ,CAAC;AAAA,IACtC;AAEA,UAAM,YAAY;AAAA,MAChB;AAAA,QACE,OAAO;AAAA,UACL;AAAA,UACA,MAAM,SAAS,WAAW,MAAM,SAAS;AAAA,UACzC,MAAM;AAAA,YACJ,MACE,SAAS,WAAW,MAChB,iBACA;AAAA,YACN,YAAY,SAAS;AAAA;AAAA;AAAA,YAGrB,QAAQ,iBAAiB;AAAA,UAC3B;AAAA,QAEF;AAAA,MACF;AAAA,IACF;AACA,WAAO,IAAI,SAAS,KAAK,UAAU,SAAS,GAAG;AAAA,MAC7C,QAAQ,SAAS;AAAA,MACjB,SAAS,EAAE,gBAAgB,mBAAmB;AAAA,IAChD,CAAC;AAAA,EACH,IACA;AAEJ,SAAO,iBAA4B;AAAA,IACjC,OAAO;AAAA,MACL,cAAc;AAAA,QACZ;AAAA,QACA,aAAa;AAAA,QACb,OAAO;AAAA,QACP,SAAS,OAAO;AAAA;AAAA,UAEd,GAAI,QAAQ,eAAe,EAAE,mBAAmB,QAAQ,aAAa,IAAI,CAAC;AAAA;AAAA,UAE1E,GAAI,UAAU,CAAC,QAAQ,eAAe,EAAE,eAAe,UAAU,MAAM,GAAG,IAAI,CAAC;AAAA,UAC/E,GAAI,cAAc,EAAE,kBAAkB,YAAY,IAAI,CAAC;AAAA,UACvD,GAAG;AAAA,QACL;AAAA,MACF,CAAC;AAAA,IACH;AAAA,EACF,CAAC;AACH;","names":[]}
1
+ {"version":3,"sources":["../src/client.ts","../src/templates.ts"],"sourcesContent":["/**\n * @synap-core/sdk — Type-safe tRPC client for Synap data pods.\n *\n * Usage:\n * ```typescript\n * import { createSynapClient } from \"@synap-core/sdk\";\n *\n * const synap = createSynapClient({\n * podUrl: \"https://pod.example.com\",\n * sessionToken: \"<Kratos session token>\",\n * workspaceId: \"ws_...\",\n * });\n *\n * // Fully typed — autocomplete works on all 47+ routers\n * const entities = await synap.entities.list.query({ limit: 20 });\n * const workspace = await synap.workspaces.get.query();\n * ```\n */\n\nimport {\n createTRPCClient,\n httpBatchLink,\n type CreateTRPCClient,\n} from \"@trpc/client\";\nimport superjson from \"superjson\";\nimport type { AppRouter } from \"@synap-core/api-types\";\n\nexport interface SynapClientOptions {\n /**\n * Base URL of the data pod (no trailing slash, no `/trpc` suffix).\n * @example \"https://pod.example.com\"\n */\n podUrl: string;\n\n /**\n * Ory Kratos session token (the `session_token` a Kratos API / native\n * login flow returns).\n * Sent as `X-Session-Token` header — preferred for non-browser clients\n * like extensions, CLI tools, and mobile apps.\n * @example \"oathkeeper_token_...\"\n */\n sessionToken?: string;\n\n /**\n * LOCAL_MODE token only (the desktop host's `LOCAL_AUTH_TOKEN`), sent as\n * `Authorization: Bearer`. `/trpc` sits behind Ory session auth, so a real\n * pod refuses hub / user API keys (`synap_hub_*`, `synap_user_*`) with a 401;\n * this client throws early if given one. Use `@synap-core/hub-rest-client`\n * against `/api/hub/*` for hub / user API keys, or `sessionToken` / cookies\n * for a signed-in user.\n * If omitted, the client falls back to cookie-based Kratos session auth.\n */\n apiKey?: string;\n\n /**\n * Workspace ID to scope all requests to.\n * Pass empty string for procedures that don't require workspace context\n * (e.g. workspaces.list, which is protectedProcedure only).\n * @example \"ws_01jk...\"\n */\n workspaceId: string;\n\n /**\n * Additional headers to include in every request.\n */\n extraHeaders?: Record<string, string>;\n\n /**\n * Called when the pod returns a `401 Unauthorized` (expired/invalid session\n * or API key). The app should sign out and re-authenticate (re-handshake).\n *\n * The pod's auth middleware can reply with a plain-JSON body\n * (`{\"error\":\"Invalid session\"}`) that is NOT in tRPC batch format and would\n * otherwise crash the superjson transformer; when an `onAuthError` handler is\n * provided the client rewrites such responses into a well-formed tRPC error so\n * the query rejects gracefully instead of throwing a parse error.\n */\n onAuthError?: (error: { status: number; message: string }) => void;\n}\n\n/**\n * Prefixes of hub / user API keys (SSOT: `KEY_PREFIXES` in synap-backend\n * `packages/database/src/schema/api-keys.ts`; hub_inbound keys use `synap_hub_`).\n * The LOCAL_MODE token is random bytes and never starts with these.\n */\nconst HUB_KEY_PREFIXES = [\"synap_hub_\", \"synap_user_\"] as const;\n\n/**\n * Creates a fully-typed tRPC client connected to a Synap data pod.\n *\n * All 47+ tRPC routers are available with full TypeScript autocomplete\n * and compile-time safety. Auth is handled via Bearer token + workspace header.\n */\nexport function createSynapClient(\n options: SynapClientOptions,\n): CreateTRPCClient<AppRouter> {\n const { podUrl, apiKey, workspaceId, extraHeaders, onAuthError } = options;\n\n if (apiKey && HUB_KEY_PREFIXES.some((p) => apiKey.startsWith(p))) {\n throw new Error(\n \"createSynapClient: this is a hub/user API key; /trpc only accepts a session or the local-mode token. \" +\n \"Use @synap-core/hub-rest-client against /api/hub/* instead.\",\n );\n }\n\n const url = `${podUrl.replace(/\\/$/, \"\")}/trpc`;\n\n // Base fetch: always include credentials so session cookies are sent in\n // cookie-auth mode. When apiKey is present, the Bearer token takes precedence\n // on the pod.\n const baseFetch: typeof fetch = (input, init) =>\n fetch(input, { ...init, credentials: \"include\" });\n\n // When an onAuthError handler is provided, wrap fetch to catch 401 (and other\n // non-tRPC error) responses. The pod's auth middleware replies with a plain\n // JSON body that is not a tRPC batch array; left untouched it crashes the\n // superjson transformer. We notify the handler and rewrite the body into a\n // valid tRPC error envelope so the query rejects cleanly.\n const wrappedFetch: typeof fetch = onAuthError\n ? async (input, init) => {\n const response = await baseFetch(input, init);\n if (response.ok) return response;\n\n const cloned = response.clone();\n let body = \"\";\n try {\n body = await cloned.text();\n } catch {\n body = \"\";\n }\n\n let isTrpcFormat = false;\n try {\n isTrpcFormat = Array.isArray(JSON.parse(body));\n } catch {\n isTrpcFormat = false;\n }\n if (isTrpcFormat) return response;\n\n const parsedMessage = (() => {\n try {\n const parsed = JSON.parse(body) as {\n error?: string;\n message?: string;\n };\n return parsed.error ?? parsed.message ?? null;\n } catch {\n return null;\n }\n })();\n // Never forward a server-controlled string as user-facing copy — map by\n // status. The raw server detail is preserved (untrusted) on `data.detail`.\n const message =\n response.status === 401\n ? \"Session expired. Please sign in again.\"\n : `Request failed (${response.status}).`;\n\n if (response.status === 401) {\n onAuthError({ status: 401, message });\n }\n\n const trpcError = [\n {\n error: {\n message,\n code: response.status === 401 ? -32001 : -32000,\n data: {\n code:\n response.status === 401\n ? \"UNAUTHORIZED\"\n : \"INTERNAL_SERVER_ERROR\",\n httpStatus: response.status,\n // Server-supplied detail — untrusted; consumers must escape it\n // before rendering (never inject as HTML).\n detail: parsedMessage ?? undefined,\n },\n\n },\n },\n ];\n return new Response(JSON.stringify(trpcError), {\n status: response.status,\n headers: { \"content-type\": \"application/json\" },\n });\n }\n : baseFetch;\n\n return createTRPCClient<AppRouter>({\n links: [\n httpBatchLink({\n url,\n transformer: superjson,\n fetch: wrappedFetch,\n headers: () => ({\n // Session token mode (Kratos, preferred for non-browser clients)\n ...(options.sessionToken ? { \"x-session-token\": options.sessionToken } : {}),\n // LOCAL_MODE token (Bearer); hub/user API keys are rejected above\n ...(apiKey && !options.sessionToken ? { Authorization: `Bearer ${apiKey}` } : {}),\n ...(workspaceId ? { \"x-workspace-id\": workspaceId } : {}),\n ...extraHeaders,\n }),\n }),\n ],\n });\n}\n\nexport type SynapClient = ReturnType<typeof createSynapClient>;\n\n// Re-export type helpers so consumers don't need @trpc/server\nexport type { AppRouter };\n","/**\n * ensureTemplate — an app asks the pod whether the template it runs on is\n * installed, and (optionally) asks for it to be installed.\n *\n * TEMPLATE-LIFECYCLE Contract B. ONE door: tRPC `workspaces.ensureTemplate`\n * (Hub REST mirror `POST /api/hub/workspaces/ensure-template`). The pod\n * resolves the definition from the catalog and installs through its governed\n * install door — an app NEVER sends a definition. \"Installed\" means an ACTIVE\n * workspace whose `settings.packageSlug` is the slug.\n *\n * The result union below is HAND-WRITTEN from the contract\n * (`TEMPLATE-LIFECYCLE/DESIGN.md`, Contract B) because the published\n * `@synap-core/api-types` does not carry the procedure until the next regen.\n * Once it does, `EnsureTemplateResult` should become\n * `RouterOutputs[\"workspaces\"][\"ensureTemplate\"]`.\n *\n * A failed call REJECTS. It is never folded into `missing`: \"the pod said the\n * template is not installed\" and \"we could not ask the pod\" are different\n * facts, and only the first may offer an install.\n */\n\nexport interface EnsureTemplateInput {\n slug: string;\n /** Ask the pod to install it when missing. */\n install?: boolean;\n /** One plain sentence: why the app needs it (shown on the install request). */\n reason?: string;\n}\n\nexport interface EnsureTemplateMissingTemplate {\n slug: string;\n name: string;\n description: string | null;\n icon: string | null;\n}\n\nexport type EnsureTemplateResult =\n /** Installed: an ACTIVE workspace carries the template. */\n | { status: \"installed\"; workspaceId: string; version: string | null; drifted: boolean }\n /** Not installed, and no install was asked. */\n | { status: \"missing\"; template: EnsureTemplateMissingTemplate }\n /** Install asked by an agent or app key: the governed install filed a proposal. */\n | { status: \"proposed\"; proposalId: string; reviewUrl: string }\n /** Install asked by a person: it is being installed now. */\n | { status: \"installing\"; workspaceId: string }\n /**\n * Another live space already holds this template's domain (one space per\n * domain), so the pod will not install a twin. The person decides what to\n * do with that space; `guidance` is the pod's own sentence.\n */\n | { status: \"exists\"; workspaceId: string; workspaceName: string; guidance: string }\n /** The catalog retired the template; `replacedBy` is its successor, if any. */\n | { status: \"retired\"; replacedBy: string | null }\n /** A COMPLETE catalog sync does not list the slug. */\n | { status: \"not_in_catalog\" }\n /** The pod cannot tell yet (catalog cache cold or last sync incomplete). */\n | { status: \"unknown\" };\n\nexport type EnsureTemplateStatus = EnsureTemplateResult[\"status\"];\n\n/**\n * The slice of the tRPC client this helper needs. Structural on purpose: it\n * accepts `createSynapClient(...)` and a React `trpc.useUtils().client` alike,\n * and keeps compiling before the procedure lands in the generated types.\n */\nexport interface EnsureTemplateClient {\n workspaces: {\n ensureTemplate: {\n mutate(input: EnsureTemplateInput): Promise<unknown>;\n };\n };\n}\n\nexport interface EnsureTemplateOptions {\n install?: boolean;\n reason?: string;\n}\n\nconst STATUSES: ReadonlySet<string> = new Set<EnsureTemplateStatus>([\n \"installed\",\n \"missing\",\n \"proposed\",\n \"installing\",\n \"exists\",\n \"retired\",\n \"not_in_catalog\",\n \"unknown\",\n]);\n\n/**\n * Narrow the wire answer. An answer whose `status` is not one the contract\n * names is a protocol error and throws: guessing \"missing\" would offer an\n * install the pod never said was needed.\n */\nexport function parseEnsureTemplateResult(raw: unknown): EnsureTemplateResult {\n const status =\n raw && typeof raw === \"object\" ? (raw as { status?: unknown }).status : undefined;\n if (typeof status !== \"string\" || !STATUSES.has(status)) {\n throw new Error(\n `ensureTemplate: the pod answered with an unknown status (${String(status)}).`,\n );\n }\n return raw as EnsureTemplateResult;\n}\n\nexport async function ensureTemplate(\n client: EnsureTemplateClient,\n slug: string,\n opts: EnsureTemplateOptions = {},\n): Promise<EnsureTemplateResult> {\n const input: EnsureTemplateInput = { slug };\n if (opts.install) input.install = true;\n if (opts.reason) input.reason = opts.reason;\n return parseEnsureTemplateResult(await client.workspaces.ensureTemplate.mutate(input));\n}\n\n/** The workspace the template lives in, when the answer names one. */\nexport function ensuredWorkspaceId(result: EnsureTemplateResult): string | null {\n return result.status === \"installed\" || result.status === \"installing\"\n ? result.workspaceId\n : null;\n}\n"],"mappings":"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","names":[]}