@synap-core/sdk 1.0.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/README.md +82 -4
- package/dist/client-C5sF20f1.d.cts +253 -0
- package/dist/client-C5sF20f1.d.ts +253 -0
- package/dist/index.cjs +48 -6
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +118 -7
- package/dist/index.d.ts +118 -7
- package/dist/index.js +41 -2
- package/dist/index.js.map +1 -1
- package/dist/public.cjs +627 -0
- package/dist/public.cjs.map +1 -0
- package/dist/public.d.cts +228 -0
- package/dist/public.d.ts +228 -0
- package/dist/public.js +578 -0
- package/dist/public.js.map +1 -0
- package/dist/testing.cjs +386 -0
- package/dist/testing.cjs.map +1 -0
- package/dist/testing.d.cts +110 -0
- package/dist/testing.d.ts +110 -0
- package/dist/testing.js +352 -0
- package/dist/testing.js.map +1 -0
- package/package.json +21 -9
- package/src/client.ts +24 -7
- package/src/index.ts +13 -0
- package/src/public/client.ts +579 -0
- package/src/public/form-model.ts +210 -0
- package/src/public/index.ts +12 -0
- package/src/public/safe-markdown.ts +114 -0
- package/src/public/share-model.ts +47 -0
- package/src/public/site-model.ts +217 -0
- package/src/templates.ts +122 -0
- package/src/testing/fixtures.ts +132 -0
- package/src/testing/index.ts +354 -0
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
|
|
32
|
-
__export(
|
|
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(
|
|
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
|
-
//
|
|
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
|
package/dist/index.cjs.map
CHANGED
|
@@ -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
|
-
*
|
|
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 (
|
|
32
|
-
*
|
|
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
|
-
*
|
|
40
|
-
*
|
|
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
|
-
|
|
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
|
-
*
|
|
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 (
|
|
32
|
-
*
|
|
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
|
-
*
|
|
40
|
-
*
|
|
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
|
-
|
|
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
|
-
//
|
|
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":[]}
|