@foir/sdk 0.1.0 → 0.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/push.d.ts ADDED
@@ -0,0 +1,109 @@
1
+ import { TypedDocumentNode } from '@graphql-typed-document-node/core';
2
+
3
+ /**
4
+ * Foir GraphQL client — a single typed fetch wrapper for SSR/edge loaders.
5
+ *
6
+ * No GraphQL client (Apollo/urql) required: the Foir public API is a plain
7
+ * `POST` with an `x-api-key` header, and the dominant consumers (Hydrogen on
8
+ * Oxygen, React Router loaders, TanStack Start on CF Workers) fetch in
9
+ * server-only loaders and render — so Apollo's `InMemoryCache`/`useQuery`
10
+ * reactivity is dead weight, and a shared cache on a stateless multi-tenant
11
+ * edge worker is a cross-tenant hazard. This helper carries only the
12
+ * Foir-specific transport concerns (auth header, preview schema channel,
13
+ * error shape). Consumers on a GraphQL client (Apollo / urql / react-query /
14
+ * graphql-request) use the generated `TypedDocumentNode`s directly with their
15
+ * own client and replicate the two Foir-specific headers below — the
16
+ * `x-api-key` and, in preview, `X-Foir-Schema-Channel: draft`.
17
+ *
18
+ * Deduplicates the hand-copied `lib/foir/client.ts` that shipped identically
19
+ * across the Foir storefronts.
20
+ */
21
+
22
+ interface FoirEnv {
23
+ PLATFORM_API_URL?: string;
24
+ PLATFORM_API_KEY?: string;
25
+ PLATFORM_PREVIEW_API_KEY?: string;
26
+ FOIR_PREVIEW?: string;
27
+ }
28
+
29
+ /**
30
+ * Foir customer auth — framework-agnostic flows, token lifecycle, and guards.
31
+ *
32
+ * Session I/O is injected via {@link SessionAdapter} (no Hydrogen/React-Router
33
+ * coupling). Pre-login flows authenticate with the project `x-api-key`;
34
+ * post-login reads add the customer bearer. The project-specific profile (e.g.
35
+ * `isAdmin`) is resolved by a consumer-supplied `resolveUser` — the SDK never
36
+ * hardcodes the per-project customer profile shape.
37
+ */
38
+
39
+ /**
40
+ * Injected session store. Methods may be sync or async so consumers can plug
41
+ * in a cookie session (Hydrogen/Remix), a KV store, etc.
42
+ */
43
+ interface SessionAdapter {
44
+ get(key: string): string | null | undefined | Promise<string | null | undefined>;
45
+ set(key: string, value: string): void | Promise<void>;
46
+ unset(key: string): void | Promise<void>;
47
+ }
48
+
49
+ interface EnableWebPushOptions {
50
+ adapter: SessionAdapter;
51
+ env: FoirEnv;
52
+ /** Path the service worker is served from (must be same-origin). Default `/foir-sw.js`. */
53
+ swPath?: string;
54
+ /** Service-worker registration scope. Default `/`. */
55
+ scope?: string;
56
+ /** Optional label stored alongside the device token. */
57
+ deviceName?: string;
58
+ }
59
+ type EnableWebPushResult = {
60
+ ok: true;
61
+ subscription: PushSubscription;
62
+ } | {
63
+ ok: false;
64
+ reason: 'unsupported' | 'denied' | 'no-config';
65
+ };
66
+ interface DisableWebPushOptions {
67
+ adapter: SessionAdapter;
68
+ env: FoirEnv;
69
+ /** Scope the service worker was registered with. Default `/`. */
70
+ scope?: string;
71
+ }
72
+ /** True when the current browser supports the Web Push APIs this helper needs. */
73
+ declare function isWebPushSupported(): boolean;
74
+ /**
75
+ * Subscribe the current browser to Foir Web Push and register the subscription
76
+ * as a `WEB` device token. Idempotent: reuses an existing subscription when one
77
+ * is already present. Returns `{ ok: false }` (never throws) when the browser
78
+ * is unsupported or the user declines permission.
79
+ */
80
+ declare function enableWebPush(opts: EnableWebPushOptions): Promise<EnableWebPushResult>;
81
+ /**
82
+ * Unsubscribe the current browser from Web Push and deregister the device
83
+ * token. Returns `false` when there was no active subscription to remove.
84
+ */
85
+ declare function disableWebPush(opts: DisableWebPushOptions): Promise<boolean>;
86
+
87
+ /** The project's non-secret VAPID public key a browser subscribes with. */
88
+ declare const WebPushConfigDocument: TypedDocumentNode<{
89
+ webPushConfig: {
90
+ vapidPublicKey: string;
91
+ } | null;
92
+ }, Record<string, never>>;
93
+ interface RegisterDeviceTokenInput {
94
+ token: string;
95
+ platform: 'IOS' | 'ANDROID' | 'WEB';
96
+ deviceName?: string;
97
+ }
98
+ declare const RegisterDeviceTokenDocument: TypedDocumentNode<{
99
+ registerDeviceToken: boolean;
100
+ }, {
101
+ input: RegisterDeviceTokenInput;
102
+ }>;
103
+ declare const UnregisterDeviceTokenDocument: TypedDocumentNode<{
104
+ unregisterDeviceToken: boolean;
105
+ }, {
106
+ token: string;
107
+ }>;
108
+
109
+ export { type DisableWebPushOptions, type EnableWebPushOptions, type EnableWebPushResult, RegisterDeviceTokenDocument, type RegisterDeviceTokenInput, UnregisterDeviceTokenDocument, WebPushConfigDocument, disableWebPush, enableWebPush, isWebPushSupported };
package/dist/push.js ADDED
@@ -0,0 +1,259 @@
1
+ // src/graphql/foir-request.ts
2
+ import { print } from "graphql";
3
+ function isFoirPreview(env) {
4
+ return env.FOIR_PREVIEW === "true";
5
+ }
6
+ var FoirRequestError = class extends Error {
7
+ /** HTTP status code, when the request failed at the HTTP layer. */
8
+ status;
9
+ /** GraphQL errors, when the request failed with a no-data error envelope. */
10
+ graphQLErrors;
11
+ cause;
12
+ constructor(message, options) {
13
+ super(message);
14
+ this.name = "FoirRequestError";
15
+ this.cause = options?.cause;
16
+ this.status = options?.status;
17
+ this.graphQLErrors = options?.graphQLErrors;
18
+ }
19
+ };
20
+ async function foirRequest(env, document, variables, options) {
21
+ const endpoint = env.PLATFORM_API_URL;
22
+ const apiKey = isFoirPreview(env) ? env.PLATFORM_PREVIEW_API_KEY ?? env.PLATFORM_API_KEY : env.PLATFORM_API_KEY;
23
+ if (!endpoint || !apiKey) {
24
+ throw new FoirRequestError(
25
+ "[foir] PLATFORM_API_URL or PLATFORM_API_KEY is not set \u2014 add them to .env"
26
+ );
27
+ }
28
+ const headers = {
29
+ "Content-Type": "application/json",
30
+ "x-api-key": apiKey
31
+ };
32
+ if (isFoirPreview(env)) {
33
+ headers["X-Foir-Schema-Channel"] = "draft";
34
+ }
35
+ if (options?.bearerToken) {
36
+ headers["Authorization"] = `Bearer ${options.bearerToken}`;
37
+ }
38
+ let res;
39
+ try {
40
+ res = await fetch(endpoint, {
41
+ method: "POST",
42
+ headers,
43
+ body: JSON.stringify({ query: print(document), variables })
44
+ });
45
+ } catch (err) {
46
+ throw new FoirRequestError("[foir] network error", { cause: err });
47
+ }
48
+ if (!res.ok) {
49
+ throw new FoirRequestError(`[foir] HTTP ${res.status} ${res.statusText}`, {
50
+ status: res.status
51
+ });
52
+ }
53
+ const json = await res.json();
54
+ if (json.errors?.length) {
55
+ const msg = json.errors.map((e) => e.message).join("; ");
56
+ if (json.data) {
57
+ console.warn(`[foir] GraphQL partial errors (data returned): ${msg}`);
58
+ } else {
59
+ throw new FoirRequestError(`[foir] GraphQL errors: ${msg}`, {
60
+ graphQLErrors: json.errors
61
+ });
62
+ }
63
+ }
64
+ if (!json.data) {
65
+ throw new FoirRequestError("[foir] empty GraphQL response");
66
+ }
67
+ return json.data;
68
+ }
69
+
70
+ // src/auth/operations.ts
71
+ import { parse } from "graphql";
72
+ var AUTH_RESULT_FIELDS = "token refreshToken customerId email";
73
+ var CustomerLoginDocument = parse(
74
+ `mutation CustomerLogin($email: String!, $password: String!) {
75
+ customerLogin(email: $email, password: $password) { ${AUTH_RESULT_FIELDS} }
76
+ }`
77
+ );
78
+ var CustomerRegisterDocument = parse(
79
+ `mutation CustomerRegister($email: String!, $password: String!) {
80
+ customerRegister(email: $email, password: $password) { ${AUTH_RESULT_FIELDS} }
81
+ }`
82
+ );
83
+ var CustomerRefreshTokenDocument = parse(
84
+ `mutation CustomerRefreshToken($refreshToken: String!) {
85
+ customerRefreshToken(refreshToken: $refreshToken) { ${AUTH_RESULT_FIELDS} }
86
+ }`
87
+ );
88
+ var CustomerRequestOTPDocument = parse(
89
+ `mutation CustomerRequestOTP($email: String!) {
90
+ customerRequestOTP(email: $email)
91
+ }`
92
+ );
93
+ var CustomerLoginOTPDocument = parse(
94
+ `mutation CustomerLoginOTP($email: String!, $otp: String!) {
95
+ customerLoginOTP(email: $email, otp: $otp) { ${AUTH_RESULT_FIELDS} }
96
+ }`
97
+ );
98
+ var CustomerLoginWithProviderDocument = parse(
99
+ `mutation CustomerLoginWithProvider($input: ProviderLoginInput!) {
100
+ customerLoginWithProvider(input: $input) { redirectUrl token }
101
+ }`
102
+ );
103
+ var CustomerProviderCallbackDocument = parse(
104
+ `mutation CustomerProviderCallback($input: ProviderCallbackInput!) {
105
+ customerProviderCallback(input: $input) { ${AUTH_RESULT_FIELDS} }
106
+ }`
107
+ );
108
+ var CustomerProviderVerifyOTPDocument = parse(
109
+ `mutation CustomerProviderVerifyOTP($input: ProviderOTPVerifyInput!) {
110
+ customerProviderVerifyOTP(input: $input) { ${AUTH_RESULT_FIELDS} }
111
+ }`
112
+ );
113
+ var AuthProvidersDocument = parse(
114
+ `query AuthProviders {
115
+ authProviders { key name type isDefault }
116
+ }`
117
+ );
118
+
119
+ // src/auth/auth.ts
120
+ var TOKEN_KEY = "foir:token";
121
+ var REFRESH_KEY = "foir:refreshToken";
122
+ var CUSTOMER_ID_KEY = "foir:customerId";
123
+ var EMAIL_KEY = "foir:email";
124
+ async function customerRefreshToken(env, refreshToken) {
125
+ return (await foirRequest(env, CustomerRefreshTokenDocument, { refreshToken })).customerRefreshToken;
126
+ }
127
+ async function saveSession(adapter, result) {
128
+ await adapter.set(TOKEN_KEY, result.token);
129
+ await adapter.set(REFRESH_KEY, result.refreshToken);
130
+ await adapter.set(CUSTOMER_ID_KEY, result.customerId);
131
+ await adapter.set(EMAIL_KEY, result.email);
132
+ return { id: result.customerId, email: result.email };
133
+ }
134
+ async function clearSession(adapter) {
135
+ await adapter.unset(TOKEN_KEY);
136
+ await adapter.unset(REFRESH_KEY);
137
+ await adapter.unset(CUSTOMER_ID_KEY);
138
+ await adapter.unset(EMAIL_KEY);
139
+ }
140
+ async function getSessionToken(adapter) {
141
+ return await adapter.get(TOKEN_KEY) ?? null;
142
+ }
143
+ async function refreshSession(adapter, env) {
144
+ const refreshToken = await adapter.get(REFRESH_KEY) ?? null;
145
+ if (!refreshToken) return null;
146
+ try {
147
+ const result = await customerRefreshToken(env, refreshToken);
148
+ await saveSession(adapter, result);
149
+ return { token: result.token, refreshToken: result.refreshToken };
150
+ } catch {
151
+ await clearSession(adapter);
152
+ return null;
153
+ }
154
+ }
155
+ function isAuthExpiredError(err) {
156
+ if (!(err instanceof FoirRequestError)) return false;
157
+ if (err.status === 401) return true;
158
+ return (err.graphQLErrors ?? []).some(
159
+ (e) => /authentication required/i.test(e.message)
160
+ );
161
+ }
162
+ async function authedRequest(adapter, env, document, variables) {
163
+ const token = await getSessionToken(adapter);
164
+ try {
165
+ return await foirRequest(env, document, variables, {
166
+ bearerToken: token ?? void 0
167
+ });
168
+ } catch (err) {
169
+ if (!isAuthExpiredError(err)) throw err;
170
+ const refreshed = await refreshSession(adapter, env);
171
+ if (!refreshed) throw err;
172
+ return foirRequest(env, document, variables, {
173
+ bearerToken: refreshed.token
174
+ });
175
+ }
176
+ }
177
+
178
+ // src/push/operations.ts
179
+ import { parse as parse2 } from "graphql";
180
+ var WebPushConfigDocument = parse2(
181
+ `query WebPushConfig {
182
+ webPushConfig { vapidPublicKey }
183
+ }`
184
+ );
185
+ var RegisterDeviceTokenDocument = parse2(
186
+ `mutation RegisterDeviceToken($input: RegisterDeviceTokenInput!) {
187
+ registerDeviceToken(input: $input)
188
+ }`
189
+ );
190
+ var UnregisterDeviceTokenDocument = parse2(
191
+ `mutation UnregisterDeviceToken($token: String!) {
192
+ unregisterDeviceToken(token: $token)
193
+ }`
194
+ );
195
+
196
+ // src/push/web-push.ts
197
+ function urlBase64ToUint8Array(base64) {
198
+ const padding = "=".repeat((4 - base64.length % 4) % 4);
199
+ const normalized = (base64 + padding).replace(/-/g, "+").replace(/_/g, "/");
200
+ const raw = atob(normalized);
201
+ const out = new Uint8Array(new ArrayBuffer(raw.length));
202
+ for (let i = 0; i < raw.length; i += 1) out[i] = raw.charCodeAt(i);
203
+ return out;
204
+ }
205
+ function isWebPushSupported() {
206
+ return typeof window !== "undefined" && typeof navigator !== "undefined" && "serviceWorker" in navigator && "PushManager" in window && "Notification" in window;
207
+ }
208
+ async function enableWebPush(opts) {
209
+ if (!isWebPushSupported()) return { ok: false, reason: "unsupported" };
210
+ const permission = await Notification.requestPermission();
211
+ if (permission !== "granted") return { ok: false, reason: "denied" };
212
+ const registration = await navigator.serviceWorker.register(
213
+ opts.swPath ?? "/foir-sw.js",
214
+ { scope: opts.scope ?? "/" }
215
+ );
216
+ await navigator.serviceWorker.ready;
217
+ const config = await authedRequest(
218
+ opts.adapter,
219
+ opts.env,
220
+ WebPushConfigDocument
221
+ );
222
+ const vapidPublicKey = config.webPushConfig?.vapidPublicKey;
223
+ if (!vapidPublicKey) return { ok: false, reason: "no-config" };
224
+ const existing = await registration.pushManager.getSubscription();
225
+ const subscription = existing ?? await registration.pushManager.subscribe({
226
+ userVisibleOnly: true,
227
+ applicationServerKey: urlBase64ToUint8Array(vapidPublicKey)
228
+ });
229
+ await authedRequest(opts.adapter, opts.env, RegisterDeviceTokenDocument, {
230
+ input: {
231
+ token: JSON.stringify(subscription),
232
+ platform: "WEB",
233
+ deviceName: opts.deviceName
234
+ }
235
+ });
236
+ return { ok: true, subscription };
237
+ }
238
+ async function disableWebPush(opts) {
239
+ if (!isWebPushSupported()) return false;
240
+ const registration = await navigator.serviceWorker.getRegistration(
241
+ opts.scope ?? "/"
242
+ );
243
+ const subscription = await registration?.pushManager.getSubscription();
244
+ if (!subscription) return false;
245
+ const token = JSON.stringify(subscription);
246
+ await subscription.unsubscribe();
247
+ await authedRequest(opts.adapter, opts.env, UnregisterDeviceTokenDocument, {
248
+ token
249
+ });
250
+ return true;
251
+ }
252
+ export {
253
+ RegisterDeviceTokenDocument,
254
+ UnregisterDeviceTokenDocument,
255
+ WebPushConfigDocument,
256
+ disableWebPush,
257
+ enableWebPush,
258
+ isWebPushSupported
259
+ };
package/dist/server.d.ts CHANGED
@@ -226,6 +226,83 @@ declare function verifyScopedToken(token: string, jwksUrl: string, options?: Ver
226
226
  */
227
227
  declare function parseContextHeader(raw: string | null): Record<string, string>;
228
228
 
229
+ /**
230
+ * requireScopedToken — the one-line ingress guard for BFF routes the
231
+ * platform dispatches operations to (EID-498).
232
+ *
233
+ * Operation endpoints must be publicly reachable (the platform's dispatcher
234
+ * refuses private networks), so "the BFF is never public-facing" is achieved
235
+ * by AUTHENTICATED ingress, not network privacy: every route the platform
236
+ * calls verifies the platform-minted `X-Foir-Token` before doing anything. A
237
+ * route that skips verification is an open endpoint anyone can call
238
+ * directly, bypassing the operation's scopes, quotas, and preconditions.
239
+ *
240
+ * ```ts
241
+ * // Hono / Workers / Hydrogen — any fetch-API request:
242
+ * const { claims, subject, context } = await requireScopedToken(request, {
243
+ * expectedProjectId: env.FOIR_PROJECT_ID,
244
+ * });
245
+ * ```
246
+ *
247
+ * Throws {@link ScopedTokenError} with `status` 401 (missing/invalid token)
248
+ * or 403 (valid token for the wrong tenant/project) — map it to a response
249
+ * in one place:
250
+ *
251
+ * ```ts
252
+ * catch (err) {
253
+ * if (err instanceof ScopedTokenError) {
254
+ * return new Response(err.message, { status: err.status });
255
+ * }
256
+ * throw err;
257
+ * }
258
+ * ```
259
+ */
260
+
261
+ /**
262
+ * Verification failure with the HTTP status the route should answer with:
263
+ * 401 for a missing or invalid token, 403 for a valid token bound to the
264
+ * wrong tenant or project.
265
+ */
266
+ declare class ScopedTokenError extends Error {
267
+ readonly status: 401 | 403;
268
+ constructor(message: string, status: 401 | 403);
269
+ }
270
+ /** The minimal request shape needed — satisfied by any fetch-API Request. */
271
+ interface HeaderCarrier {
272
+ headers: {
273
+ get(name: string): string | null;
274
+ };
275
+ }
276
+ interface RequireScopedTokenOptions extends VerifyScopedTokenOptions {
277
+ /**
278
+ * JWKS endpoint to verify against. Defaults to the production platform
279
+ * (`https://api.foir.dev/.well-known/jwks.json`); point it at your dev
280
+ * host's `/.well-known/jwks.json` for non-production deployments.
281
+ */
282
+ jwksUrl?: string;
283
+ /**
284
+ * When set, a verified token whose subject names a different project is
285
+ * rejected with 403. Set this from your deploy's own config — it pins the
286
+ * route to its project even if a token minted for another project leaks.
287
+ */
288
+ expectedProjectId?: string;
289
+ /** Tenant analogue of {@link expectedProjectId}. */
290
+ expectedTenantId?: string;
291
+ }
292
+ interface ScopedTokenIngress {
293
+ claims: ScopedTokenClaims;
294
+ subject: ScopedTokenSubject;
295
+ /** Parsed `X-Foir-Context` correlation metadata (empty when absent). */
296
+ context: Record<string, string>;
297
+ }
298
+ /**
299
+ * Verify the platform-minted scoped token on an inbound dispatch request.
300
+ * Returns the decoded claims, parsed subject, and dispatch context; throws
301
+ * {@link ScopedTokenError} otherwise. See the module doc for the catch
302
+ * pattern.
303
+ */
304
+ declare function requireScopedToken(request: HeaderCarrier, options?: RequireScopedTokenOptions): Promise<ScopedTokenIngress>;
305
+
229
306
  /**
230
307
  * Type definitions for platform operations.
231
308
  *
@@ -454,4 +531,4 @@ declare function createCallbackClient<TResult = unknown>(callback: OperationCall
454
531
  fetch?: typeof fetch;
455
532
  }): CallbackClient<TResult>;
456
533
 
457
- export { type CallbackClient, type CallbackResult, type EditorPlacement, type GraphQLResponse, type OperationCallback, type OperationContext, type OperationPayload, type OperationRecordContext, type OperationResult, type OperationTrigger, type ScopedTokenClaims, type ScopedTokenSubject, type ServerClient, ServerClientError, type ServerClientOptions, type VerifyScopedTokenOptions, createCallbackClient, createServerClient, parseContextHeader, parseSubject, recordsReadModels, recordsWriteModels, verifyScopedToken, verifyWebhookSignature };
534
+ export { type CallbackClient, type CallbackResult, type EditorPlacement, type GraphQLResponse, type OperationCallback, type OperationContext, type OperationPayload, type OperationRecordContext, type OperationResult, type OperationTrigger, type RequireScopedTokenOptions, type ScopedTokenClaims, ScopedTokenError, type ScopedTokenIngress, type ScopedTokenSubject, type ServerClient, ServerClientError, type ServerClientOptions, type VerifyScopedTokenOptions, createCallbackClient, createServerClient, parseContextHeader, parseSubject, recordsReadModels, recordsWriteModels, requireScopedToken, verifyScopedToken, verifyWebhookSignature };
package/dist/server.js CHANGED
@@ -184,6 +184,67 @@ function parseContextHeader(raw) {
184
184
  return out;
185
185
  }
186
186
 
187
+ // src/server/require-scoped-token.ts
188
+ var TOKEN_HEADER = "X-Foir-Token";
189
+ var CONTEXT_HEADER = "X-Foir-Context";
190
+ var DEFAULT_JWKS_URL = "https://api.foir.dev/.well-known/jwks.json";
191
+ var ScopedTokenError = class extends Error {
192
+ status;
193
+ constructor(message, status) {
194
+ super(message);
195
+ this.name = "ScopedTokenError";
196
+ this.status = status;
197
+ }
198
+ };
199
+ async function requireScopedToken(request, options) {
200
+ const token = request.headers.get(TOKEN_HEADER);
201
+ if (!token) {
202
+ throw new ScopedTokenError(
203
+ `missing ${TOKEN_HEADER} header \u2014 this route only accepts platform-dispatched requests`,
204
+ 401
205
+ );
206
+ }
207
+ let claims;
208
+ try {
209
+ claims = await verifyScopedToken(
210
+ token,
211
+ options?.jwksUrl ?? DEFAULT_JWKS_URL,
212
+ options
213
+ );
214
+ } catch (err) {
215
+ throw new ScopedTokenError(
216
+ `invalid scoped token: ${err instanceof Error ? err.message : String(err)}`,
217
+ 401
218
+ );
219
+ }
220
+ let subject;
221
+ try {
222
+ subject = parseSubject(claims.sub);
223
+ } catch (err) {
224
+ throw new ScopedTokenError(
225
+ `invalid scoped token subject: ${err instanceof Error ? err.message : String(err)}`,
226
+ 401
227
+ );
228
+ }
229
+ if (options?.expectedProjectId && subject.projectId !== options.expectedProjectId) {
230
+ throw new ScopedTokenError(
231
+ "scoped token is bound to a different project",
232
+ 403
233
+ );
234
+ }
235
+ if (options?.expectedTenantId && subject.tenantId !== options.expectedTenantId) {
236
+ throw new ScopedTokenError(
237
+ "scoped token is bound to a different tenant",
238
+ 403
239
+ );
240
+ }
241
+ return {
242
+ claims,
243
+ subject,
244
+ context: parseContextHeader(request.headers.get(CONTEXT_HEADER))
245
+ };
246
+ }
247
+
187
248
  // src/server/callback-client.ts
188
249
  function createCallbackClient(callback, opts) {
189
250
  const { executionId, signingSecret } = opts;
@@ -284,6 +345,7 @@ async function hmacSha256Hex(secret, body) {
284
345
  return Array.from(new Uint8Array(sig)).map((b) => b.toString(16).padStart(2, "0")).join("");
285
346
  }
286
347
  export {
348
+ ScopedTokenError,
287
349
  ServerClientError,
288
350
  createCallbackClient,
289
351
  createServerClient,
@@ -291,6 +353,7 @@ export {
291
353
  parseSubject,
292
354
  recordsReadModels,
293
355
  recordsWriteModels,
356
+ requireScopedToken,
294
357
  verifyScopedToken,
295
358
  verifyWebhookSignature
296
359
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@foir/sdk",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Official Foir SDK — editor iframe client + server-side platform helpers",
5
5
  "type": "module",
6
6
  "publishConfig": {
@@ -33,6 +33,10 @@
33
33
  "types": "./dist/auth.d.ts",
34
34
  "import": "./dist/auth.js"
35
35
  },
36
+ "./push": {
37
+ "types": "./dist/push.d.ts",
38
+ "import": "./dist/push.js"
39
+ },
36
40
  "./realtime": {
37
41
  "types": "./dist/realtime.d.ts",
38
42
  "import": "./dist/realtime.js"