@foir/sdk 0.1.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/server.js ADDED
@@ -0,0 +1,296 @@
1
+ // src/server/server-client.ts
2
+ var ServerClientError = class extends Error {
3
+ constructor(message, status, response) {
4
+ super(message);
5
+ this.status = status;
6
+ this.response = response;
7
+ this.name = "ServerClientError";
8
+ }
9
+ status;
10
+ response;
11
+ };
12
+ function createServerClient(options) {
13
+ const { baseUrl, apiKey } = options;
14
+ async function request(url, init = {}) {
15
+ const res = await fetch(url, {
16
+ ...init,
17
+ headers: {
18
+ "Content-Type": "application/json",
19
+ "x-api-key": apiKey,
20
+ ...init.headers
21
+ }
22
+ });
23
+ const text = await res.text();
24
+ if (!res.ok) {
25
+ let body;
26
+ try {
27
+ body = JSON.parse(text);
28
+ } catch {
29
+ body = text;
30
+ }
31
+ throw new ServerClientError(
32
+ `Platform API error: ${res.status} ${res.statusText}`,
33
+ res.status,
34
+ body
35
+ );
36
+ }
37
+ if (!text) return void 0;
38
+ return JSON.parse(text);
39
+ }
40
+ return {
41
+ async query(query, variables) {
42
+ return request(`${baseUrl}/graphql`, {
43
+ method: "POST",
44
+ body: JSON.stringify({ query, variables })
45
+ });
46
+ }
47
+ };
48
+ }
49
+
50
+ // src/server/webhook-helpers.ts
51
+ async function verifyWebhookSignature(payload, signature, secret) {
52
+ if (!signature || !secret) return false;
53
+ const encoder = new TextEncoder();
54
+ const key = await crypto.subtle.importKey(
55
+ "raw",
56
+ encoder.encode(secret),
57
+ { name: "HMAC", hash: "SHA-256" },
58
+ false,
59
+ ["sign"]
60
+ );
61
+ const signatureBytes = await crypto.subtle.sign(
62
+ "HMAC",
63
+ key,
64
+ encoder.encode(payload)
65
+ );
66
+ const computed = Array.from(new Uint8Array(signatureBytes)).map((b) => b.toString(16).padStart(2, "0")).join("");
67
+ if (computed.length !== signature.length) return false;
68
+ let mismatch = 0;
69
+ for (let i = 0; i < computed.length; i++) {
70
+ mismatch |= computed.charCodeAt(i) ^ signature.charCodeAt(i);
71
+ }
72
+ return mismatch === 0;
73
+ }
74
+
75
+ // src/server/verify-scoped-token.ts
76
+ var JWKS_CACHE_TTL_MS = 5 * 60 * 1e3;
77
+ function getCache() {
78
+ const g = globalThis;
79
+ if (!g.__foirJwksCache) {
80
+ g.__foirJwksCache = /* @__PURE__ */ new Map();
81
+ }
82
+ return g.__foirJwksCache;
83
+ }
84
+ function base64UrlDecode(s) {
85
+ const padded = s.replace(/-/g, "+").replace(/_/g, "/").padEnd(s.length + (4 - s.length % 4) % 4, "=");
86
+ const binary = atob(padded);
87
+ const bytes = new Uint8Array(binary.length);
88
+ for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
89
+ return bytes;
90
+ }
91
+ async function loadJwks(jwksUrl) {
92
+ const cache = getCache();
93
+ const cached = cache.get(jwksUrl);
94
+ if (cached && Date.now() - cached.fetchedAt < JWKS_CACHE_TTL_MS) {
95
+ return cached.keys;
96
+ }
97
+ const res = await fetch(jwksUrl);
98
+ if (!res.ok) throw new Error(`jwks fetch ${res.status}`);
99
+ const doc = await res.json();
100
+ const keys = /* @__PURE__ */ new Map();
101
+ for (const jwk of doc.keys) {
102
+ if (jwk.kty !== "OKP" || jwk.crv !== "Ed25519") continue;
103
+ const raw = base64UrlDecode(jwk.x);
104
+ const key = await crypto.subtle.importKey(
105
+ "raw",
106
+ raw.buffer.slice(
107
+ raw.byteOffset,
108
+ raw.byteOffset + raw.byteLength
109
+ ),
110
+ { name: "Ed25519" },
111
+ false,
112
+ ["verify"]
113
+ );
114
+ keys.set(jwk.kid, key);
115
+ }
116
+ cache.set(jwksUrl, { keys, fetchedAt: Date.now() });
117
+ return keys;
118
+ }
119
+ function parseSubject(sub) {
120
+ const parts = sub.split("|");
121
+ if (parts.length !== 3) {
122
+ throw new Error(`malformed sub: expected 3 pipe-delimited segments, got ${parts.length}`);
123
+ }
124
+ const [tenantId, projectId, appName] = parts;
125
+ if (!tenantId || !projectId || !appName) {
126
+ throw new Error("malformed sub: empty segment");
127
+ }
128
+ return { tenantId, projectId, appName };
129
+ }
130
+ function recordsReadModels(claims) {
131
+ return claims.rrm ?? [];
132
+ }
133
+ function recordsWriteModels(claims) {
134
+ return claims.rwm ?? [];
135
+ }
136
+ async function verifyScopedToken(token, jwksUrl, options) {
137
+ const expectedIssuer = options?.expectedIssuer ?? "foir-platform";
138
+ const parts = token.split(".");
139
+ if (parts.length !== 3) throw new Error("malformed token");
140
+ const [rawHeader, rawPayload, rawSig] = parts;
141
+ const header = JSON.parse(
142
+ new TextDecoder().decode(base64UrlDecode(rawHeader))
143
+ );
144
+ if (header.alg !== "EdDSA") throw new Error(`unexpected alg ${header.alg}`);
145
+ if (!header.kid) throw new Error("missing kid");
146
+ const keys = await loadJwks(jwksUrl);
147
+ const key = keys.get(header.kid);
148
+ if (!key) throw new Error(`unknown kid ${header.kid}`);
149
+ const signingInput = new TextEncoder().encode(`${rawHeader}.${rawPayload}`);
150
+ const sig = base64UrlDecode(rawSig);
151
+ const ok = await crypto.subtle.verify(
152
+ { name: "Ed25519" },
153
+ key,
154
+ sig.buffer.slice(sig.byteOffset, sig.byteOffset + sig.byteLength),
155
+ signingInput.buffer.slice(
156
+ signingInput.byteOffset,
157
+ signingInput.byteOffset + signingInput.byteLength
158
+ )
159
+ );
160
+ if (!ok) throw new Error("signature invalid");
161
+ const claims = JSON.parse(
162
+ new TextDecoder().decode(base64UrlDecode(rawPayload))
163
+ );
164
+ const now = Math.floor(Date.now() / 1e3);
165
+ if (claims.iss !== expectedIssuer) throw new Error(`bad iss ${claims.iss}`);
166
+ if (typeof claims.exp !== "number" || claims.exp < now) {
167
+ throw new Error("token expired");
168
+ }
169
+ if (typeof claims.nbf === "number" && claims.nbf > now + 30) {
170
+ throw new Error("token not yet valid");
171
+ }
172
+ return claims;
173
+ }
174
+ function parseContextHeader(raw) {
175
+ if (!raw) return {};
176
+ const out = {};
177
+ for (const part of raw.split(";")) {
178
+ const eq = part.indexOf("=");
179
+ if (eq < 0) continue;
180
+ const k = part.slice(0, eq).trim();
181
+ const v = part.slice(eq + 1).trim();
182
+ if (k) out[k] = v;
183
+ }
184
+ return out;
185
+ }
186
+
187
+ // src/server/callback-client.ts
188
+ function createCallbackClient(callback, opts) {
189
+ const { executionId, signingSecret } = opts;
190
+ const fetchImpl = opts.fetch ?? globalThis.fetch;
191
+ if (!fetchImpl) {
192
+ throw new Error(
193
+ "createCallbackClient: no fetch available; pass opts.fetch on runtimes without a global fetch"
194
+ );
195
+ }
196
+ if (!executionId) throw new Error("createCallbackClient: executionId is required");
197
+ if (!signingSecret) throw new Error("createCallbackClient: signingSecret is required");
198
+ if (!callback?.token || !callback?.gqlEndpoint || !callback?.mutations) {
199
+ throw new Error("createCallbackClient: callback block is missing required fields");
200
+ }
201
+ const post = async (mutationName, fields, variables) => {
202
+ const query = `mutation ${mutationName}Mutation($id: String!${fieldArgsForMutation(mutationName, variables)}) { ${mutationName}(id: $id${fieldArgsCallSite(mutationName, variables)}) { ${fields} } }`;
203
+ const body = JSON.stringify({
204
+ query,
205
+ variables: { id: executionId, ...variables }
206
+ });
207
+ const signature = await hmacSha256Hex(signingSecret, body);
208
+ const resp = await fetchImpl(callback.gqlEndpoint, {
209
+ method: "POST",
210
+ headers: {
211
+ "Content-Type": "application/json",
212
+ Authorization: `Bearer ${callback.token}`,
213
+ "X-Foir-Signature": `sha256=${signature}`
214
+ },
215
+ body
216
+ });
217
+ if (!resp.ok) {
218
+ throw new Error(`callback ${mutationName} failed with HTTP ${resp.status}: ${await resp.text()}`);
219
+ }
220
+ const json = await resp.json();
221
+ if (json.errors?.length) {
222
+ throw new Error(`callback ${mutationName} GraphQL error: ${json.errors[0].message}`);
223
+ }
224
+ const out = json.data?.[mutationName];
225
+ if (!out) {
226
+ throw new Error(`callback ${mutationName} returned no data`);
227
+ }
228
+ return out;
229
+ };
230
+ return {
231
+ complete: (result) => post(callback.mutations.complete, "status cancelled", { result }),
232
+ fail: (code, message, options = {}) => post(callback.mutations.fail, "status cancelled", {
233
+ code,
234
+ message,
235
+ retryable: options.retryable,
236
+ details: options.details
237
+ }),
238
+ progress: (update) => post(callback.mutations.progress, "status cancelled applied", {
239
+ pct: update.pct,
240
+ message: update.message,
241
+ metadata: update.metadata
242
+ }),
243
+ cancel: () => post(callback.mutations.cancel, "status cancelled", {})
244
+ };
245
+ }
246
+ function fieldArgsForMutation(mutationName, vars) {
247
+ const parts = [];
248
+ for (const key of Object.keys(vars)) {
249
+ if (vars[key] === void 0) continue;
250
+ parts.push(`, $${key}: ${graphQLTypeFor(mutationName, key)}`);
251
+ }
252
+ return parts.join("");
253
+ }
254
+ function fieldArgsCallSite(_mutationName, vars) {
255
+ const parts = [];
256
+ for (const key of Object.keys(vars)) {
257
+ if (vars[key] === void 0) continue;
258
+ parts.push(`, ${key}: $${key}`);
259
+ }
260
+ return parts.join("");
261
+ }
262
+ function graphQLTypeFor(mutationName, key) {
263
+ if (key === "code" || key === "message") return "String!";
264
+ if (key === "pct") return "Int";
265
+ if (key === "retryable") return "Boolean";
266
+ if (key === "details" || key === "metadata") return "JSON";
267
+ if (key === "result") {
268
+ const base = mutationName.replace(/^complete/, "").replace(/Execution$/, "");
269
+ return `${base}ExecutionResultInput!`;
270
+ }
271
+ if (key === "message") return "String";
272
+ return "String";
273
+ }
274
+ async function hmacSha256Hex(secret, body) {
275
+ const enc = new TextEncoder();
276
+ const key = await crypto.subtle.importKey(
277
+ "raw",
278
+ enc.encode(secret),
279
+ { name: "HMAC", hash: "SHA-256" },
280
+ false,
281
+ ["sign"]
282
+ );
283
+ const sig = await crypto.subtle.sign("HMAC", key, enc.encode(body));
284
+ return Array.from(new Uint8Array(sig)).map((b) => b.toString(16).padStart(2, "0")).join("");
285
+ }
286
+ export {
287
+ ServerClientError,
288
+ createCallbackClient,
289
+ createServerClient,
290
+ parseContextHeader,
291
+ parseSubject,
292
+ recordsReadModels,
293
+ recordsWriteModels,
294
+ verifyScopedToken,
295
+ verifyWebhookSignature
296
+ };
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Shared introspection helpers for the Foir codegen plugins.
3
+ */
4
+
5
+ interface BlockPluginConfig {
6
+ /**
7
+ * Union type name(s) to collect block members from. Defaults to `['Block']`
8
+ * (the canonical Foir block union). A project with a distinct content-field
9
+ * segment union lists it here too.
10
+ */
11
+ unions?: string[];
12
+ /** Import path for the codegen-generated types. Default `./graphql`. */
13
+ graphqlImport?: string;
14
+ /** Import path for the SDK content primitives. Default `@foir/sdk/content`. */
15
+ sdkImport?: string;
16
+ }
17
+
18
+ export type { BlockPluginConfig as B };
package/package.json ADDED
@@ -0,0 +1,136 @@
1
+ {
2
+ "name": "@foir/sdk",
3
+ "version": "0.1.0",
4
+ "description": "Official Foir SDK — editor iframe client + server-side platform helpers",
5
+ "type": "module",
6
+ "publishConfig": {
7
+ "access": "public"
8
+ },
9
+ "main": "./dist/index.js",
10
+ "types": "./dist/index.d.ts",
11
+ "exports": {
12
+ ".": {
13
+ "types": "./dist/index.d.ts",
14
+ "import": "./dist/index.js"
15
+ },
16
+ "./server": {
17
+ "types": "./dist/server.d.ts",
18
+ "import": "./dist/server.js"
19
+ },
20
+ "./client": {
21
+ "types": "./dist/client.d.ts",
22
+ "import": "./dist/client.js"
23
+ },
24
+ "./media": {
25
+ "types": "./dist/media.d.ts",
26
+ "import": "./dist/media.js"
27
+ },
28
+ "./content": {
29
+ "types": "./dist/content.d.ts",
30
+ "import": "./dist/content.js"
31
+ },
32
+ "./auth": {
33
+ "types": "./dist/auth.d.ts",
34
+ "import": "./dist/auth.js"
35
+ },
36
+ "./realtime": {
37
+ "types": "./dist/realtime.d.ts",
38
+ "import": "./dist/realtime.js"
39
+ },
40
+ "./codegen": {
41
+ "types": "./dist/codegen/index.d.ts",
42
+ "import": "./dist/codegen/index.js"
43
+ },
44
+ "./codegen/block-registry": {
45
+ "types": "./dist/codegen/block-registry.d.ts",
46
+ "import": "./dist/codegen/block-registry.js"
47
+ },
48
+ "./codegen/block-scaffold": {
49
+ "types": "./dist/codegen/block-scaffold.d.ts",
50
+ "import": "./dist/codegen/block-scaffold.js"
51
+ }
52
+ },
53
+ "files": [
54
+ "dist"
55
+ ],
56
+ "keywords": [
57
+ "foir",
58
+ "sdk",
59
+ "cms",
60
+ "editor",
61
+ "platform"
62
+ ],
63
+ "repository": {
64
+ "type": "git",
65
+ "url": "https://github.com/eidestudio/foir-clients.git",
66
+ "directory": "packages/sdk"
67
+ },
68
+ "license": "MIT",
69
+ "dependencies": {
70
+ "@graphql-typed-document-node/core": "3.2.0"
71
+ },
72
+ "peerDependencies": {
73
+ "@graphql-codegen/cli": "^5.0.0",
74
+ "@graphql-codegen/client-preset": "^4.0.0",
75
+ "blurhash": "^2.0.0",
76
+ "graphql": "^16.0.0",
77
+ "hls.js": "^1.5.0",
78
+ "react": "^18.0.0 || ^19.0.0",
79
+ "y-websocket": "^3.0.0",
80
+ "yjs": "^13.6.0"
81
+ },
82
+ "peerDependenciesMeta": {
83
+ "@graphql-codegen/cli": {
84
+ "optional": true
85
+ },
86
+ "@graphql-codegen/client-preset": {
87
+ "optional": true
88
+ },
89
+ "blurhash": {
90
+ "optional": true
91
+ },
92
+ "graphql": {
93
+ "optional": true
94
+ },
95
+ "hls.js": {
96
+ "optional": true
97
+ },
98
+ "react": {
99
+ "optional": true
100
+ },
101
+ "y-websocket": {
102
+ "optional": true
103
+ },
104
+ "yjs": {
105
+ "optional": true
106
+ }
107
+ },
108
+ "devDependencies": {
109
+ "@graphql-codegen/cli": "^5.0.7",
110
+ "@types/react": "^19.2.0",
111
+ "@types/react-dom": "^19.2.0",
112
+ "blurhash": "^2.0.5",
113
+ "graphql": "^16.10.0",
114
+ "hls.js": "^1.6.16",
115
+ "react": "^19.2.0",
116
+ "react-dom": "^19.2.0",
117
+ "tsup": "^8.5.1",
118
+ "typescript": "5.9.2",
119
+ "vitest": "^3.2.4",
120
+ "y-websocket": "^3.0.0",
121
+ "yjs": "^13.6.30"
122
+ },
123
+ "engines": {
124
+ "node": ">=18.0.0"
125
+ },
126
+ "scripts": {
127
+ "build": "tsup",
128
+ "dev": "tsup --watch",
129
+ "check-types": "tsc --noEmit",
130
+ "test": "vitest run",
131
+ "test:watch": "vitest watch",
132
+ "release:patch": "pnpm version patch",
133
+ "release:minor": "pnpm version minor",
134
+ "release:major": "pnpm version major"
135
+ }
136
+ }