@usequeek/app-sdk 0.5.0 → 0.6.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 +172 -16
- package/dist/app-auth.d.ts +23 -2
- package/dist/app-auth.d.ts.map +1 -1
- package/dist/app-auth.js +40 -6
- package/dist/app-auth.js.map +1 -1
- package/dist/auth-fetch.d.ts +90 -0
- package/dist/auth-fetch.d.ts.map +1 -0
- package/dist/auth-fetch.js +261 -0
- package/dist/auth-fetch.js.map +1 -0
- package/dist/client.d.ts +32 -10
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +6 -2
- package/dist/client.js.map +1 -1
- package/dist/frame.d.ts +233 -4
- package/dist/frame.d.ts.map +1 -1
- package/dist/frame.js +694 -19
- package/dist/frame.js.map +1 -1
- package/dist/handoff.d.ts +32 -2
- package/dist/handoff.d.ts.map +1 -1
- package/dist/handoff.js +2 -1
- package/dist/handoff.js.map +1 -1
- package/dist/hono.d.ts +13 -0
- package/dist/hono.d.ts.map +1 -1
- package/dist/hono.js +15 -0
- package/dist/hono.js.map +1 -1
- package/dist/index.d.ts +11 -7
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +10 -6
- package/dist/index.js.map +1 -1
- package/dist/install-handlers.d.ts +33 -8
- package/dist/install-handlers.d.ts.map +1 -1
- package/dist/install-handlers.js +133 -15
- package/dist/install-handlers.js.map +1 -1
- package/dist/merchant-schema.d.ts +1550 -68
- package/dist/merchant-schema.d.ts.map +1 -1
- package/dist/merchant-schema.js +1 -1
- package/dist/proxy.d.ts +122 -0
- package/dist/proxy.d.ts.map +1 -0
- package/dist/proxy.js +204 -0
- package/dist/proxy.js.map +1 -0
- package/dist/react.d.ts +78 -0
- package/dist/react.d.ts.map +1 -0
- package/dist/react.js +262 -0
- package/dist/react.js.map +1 -0
- package/dist/scopes.d.ts +174 -0
- package/dist/scopes.d.ts.map +1 -0
- package/dist/scopes.js +297 -0
- package/dist/scopes.js.map +1 -0
- package/dist/server.d.ts +2 -2
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +1 -1
- package/dist/server.js.map +1 -1
- package/dist/session.d.ts +52 -2
- package/dist/session.d.ts.map +1 -1
- package/dist/session.js +99 -12
- package/dist/session.js.map +1 -1
- package/dist/store.d.ts +33 -0
- package/dist/store.d.ts.map +1 -1
- package/dist/store.js +114 -4
- package/dist/store.js.map +1 -1
- package/dist/theme.d.ts +96 -0
- package/dist/theme.d.ts.map +1 -0
- package/dist/theme.js +158 -0
- package/dist/theme.js.map +1 -0
- package/dist/tokens.d.ts +30 -15
- package/dist/tokens.d.ts.map +1 -1
- package/dist/tokens.js +57 -19
- package/dist/tokens.js.map +1 -1
- package/dist/webhooks.d.ts.map +1 -1
- package/dist/webhooks.js +1 -1
- package/dist/webhooks.js.map +1 -1
- package/package.json +17 -5
- package/CHANGELOG.md +0 -150
- package/openapi/merchant.json +0 -21452
package/README.md
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# @usequeek/app-sdk
|
|
2
2
|
|
|
3
|
+
> In development — not yet released. APIs may change before 1.0.
|
|
4
|
+
|
|
3
5
|
The SDK for building a Queek app on the public Merchant API and signed webhooks. Framework-agnostic Web-standard handlers for the install handoff and topic webhooks (`Request` in, `Response` out — use them from Next.js route handlers, Express, or any runtime), a typed Merchant API client (types generated from the live contract), GitHub-style app credentials (one asymmetric key per app, short-lived per-installation tokens minted on demand), resync recovery, and an encrypted installation store (SQLite for local/test, Postgres in production). Optional Hono wrappers live under `@usequeek/app-sdk/hono`.
|
|
4
6
|
|
|
5
7
|
```sh
|
|
@@ -28,7 +30,7 @@ const store = new SqliteInstallationStore({
|
|
|
28
30
|
storeKey: process.env.APP_ENCRYPTION_KEY!,
|
|
29
31
|
});
|
|
30
32
|
const tokens = createAppTokenProvider({
|
|
31
|
-
credential: loadAppCredential({ appSlug: "hello" }), // APP_SLUG/APP_KEY_ID/APP_PRIVATE_KEY
|
|
33
|
+
credential: loadAppCredential({ appSlug: "hello" }), // APP_SLUG/APP_KEY_ID/APP_PRIVATE_KEY (base64 of the PEM, one line)
|
|
32
34
|
store,
|
|
33
35
|
});
|
|
34
36
|
const installOptions = { appSecret: process.env.QUEEK_APP_SECRET!, store };
|
|
@@ -126,7 +128,7 @@ Signature verification covers the exact bytes Queek sent (`{id}.{timestamp}.{bod
|
|
|
126
128
|
|
|
127
129
|
### Body-size posture
|
|
128
130
|
|
|
129
|
-
The SDK itself imposes no maximum body size (`arrayBuffer` / `TextDecoder` / `JSON.parse` are unbounded
|
|
131
|
+
The SDK itself imposes no maximum body size (`arrayBuffer` / `TextDecoder` / `JSON.parse` are unbounded) — the cap belongs to the framework parser in front of it, which already rejects oversized bodies before the SDK ever sees them. Queek deliveries are small JSON payloads, so the framework defaults are plenty; if you set them explicitly, `express.raw({ type: "application/json", limit: "1mb" })` and Fastify's default `bodyLimit` of 1 MiB are the recommended ceilings. No SDK option is needed unless Queek ever ships large deliveries.
|
|
130
132
|
|
|
131
133
|
(If you already run Hono on Node, `@hono/node-server`'s `getRequestListener` bridges serving for you — but the adapters above need no extra dependency.)
|
|
132
134
|
|
|
@@ -162,8 +164,6 @@ app.route(
|
|
|
162
164
|
export default app;
|
|
163
165
|
```
|
|
164
166
|
|
|
165
|
-
> Migrating from 0.4.x: `import { createInstallHandlers } from "@usequeek/app-sdk/hono"` — the creators moved out of the root entry so non-Hono apps never install `hono`. The options objects are unchanged.
|
|
166
|
-
|
|
167
167
|
Every Merchant API call goes through `createInstallationClient({ installationId, apiBase, tokens })`,
|
|
168
168
|
which resolves the installation's token via `acquireToken()` and sends it as `X-Client-Key`.
|
|
169
169
|
|
|
@@ -187,7 +187,8 @@ One asymmetric credential per app — no per-installation secrets cross the hand
|
|
|
187
187
|
|
|
188
188
|
Queek prints the app secret (`QUEEK_APP_SECRET`, verifies the handoff) and the key id
|
|
189
189
|
(`APP_KEY_ID`, rides the JWT `kid` header). Configure the app with `APP_SLUG` (= `iss`),
|
|
190
|
-
`APP_KEY_ID`, and `APP_PRIVATE_KEY` (the
|
|
190
|
+
`APP_KEY_ID`, and `APP_PRIVATE_KEY` (base64 of the private key PEM, one line — the
|
|
191
|
+
Developer page shows it; raw PEM also accepted — never logged, never shipped to clients).
|
|
191
192
|
|
|
192
193
|
3. **Acquire / cache / re-mint.** `acquireToken(installationId)` serves the cached token while
|
|
193
194
|
its expiry is more than 5 minutes away; otherwise it signs an RS256 app JWT
|
|
@@ -246,10 +247,49 @@ settings) does NOT come back from resync: it needs the per-app `pg_dump` backup
|
|
|
246
247
|
(RPO ≤ 24 h) plus the `APP_ENCRYPTION_KEY` backup. Events missed beyond Queek's webhook
|
|
247
248
|
retries (~4 h) are gone; resync cannot backfill them. Full runbook: `docs/deploy.md` + O1.
|
|
248
249
|
|
|
250
|
+
## Data deletion
|
|
251
|
+
|
|
252
|
+
Treat the uninstall handoff (`app/uninstalled`) as the deletion trigger: the default
|
|
253
|
+
`onUninstall` already deletes the installation row (secrets, token cache, settings) —
|
|
254
|
+
keep that delete when you override it, and purge any app-side working data keyed by the
|
|
255
|
+
installation there too.
|
|
256
|
+
|
|
257
|
+
Unknown topics answer 200 without running a handler (forward compatibility, not consent):
|
|
258
|
+
that 200 must not swallow a future privacy topic. If Queek ships mandatory privacy
|
|
259
|
+
topics, register explicit handlers for them — an unhandled topic only reports
|
|
260
|
+
`unhandled: true`, it deletes nothing.
|
|
261
|
+
|
|
262
|
+
## Storefront app-proxy (signed reads)
|
|
263
|
+
|
|
264
|
+
Queek signs each storefront proxy fetch with the installation's `proxy_secret`
|
|
265
|
+
(`AppProxyService::signQuery`); verify before answering the shopper:
|
|
266
|
+
|
|
267
|
+
```ts
|
|
268
|
+
import { handleProxyRequest } from "@usequeek/app-sdk";
|
|
269
|
+
|
|
270
|
+
export async function GET(request: Request) {
|
|
271
|
+
return handleProxyRequest(
|
|
272
|
+
request,
|
|
273
|
+
{ store, path: "/apps/booking/availability" }, // Queek-side canonical path, not the local route
|
|
274
|
+
({ installation, params }) =>
|
|
275
|
+
Response.json({ slots: slotsFor(params.date, installation.storePid) }),
|
|
276
|
+
);
|
|
277
|
+
}
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
The canonical string is `path + "\n" + shop + "\n" + ts + "\n" + sorted(k=v&...)`
|
|
281
|
+
(`sig` excluded), hex HMAC-SHA256 over the FULL `whsec_…` string — no base64 decode
|
|
282
|
+
step. `kid` routes to the installation whose `proxy_secret` verifies (previous-secret
|
|
283
|
+
grace: pass every active secret to `verifyProxyQuery`); timestamps skew at most 5
|
|
284
|
+
minutes, each `jti` is single-use, and only `GET` is served (phase 1 is read-only).
|
|
285
|
+
Hono: mount `createProxyHandler({ store, path, onVerified })` from
|
|
286
|
+
`@usequeek/app-sdk/hono`.
|
|
287
|
+
|
|
249
288
|
## API surface
|
|
250
289
|
|
|
251
|
-
- **app-auth** (`app-auth.ts`): `loadAppCredential` (`APP_SLUG`/`APP_KEY_ID`/`APP_PRIVATE_KEY
|
|
252
|
-
|
|
290
|
+
- **app-auth** (`app-auth.ts`): `loadAppCredential` (`APP_SLUG`/`APP_KEY_ID`/`APP_PRIVATE_KEY`
|
|
291
|
+
— base64 of the PEM, raw PEM, or `\n`-escaped one-line PEM; RSA validated at boot),
|
|
292
|
+
`signAppJwt` (RS256, `iat` now − 60 s, `exp` window 540 s ≤ 600 s,
|
|
253
293
|
`kid` header), the wire-contract error codes in one place (`INVALID_CLIENT_CODE`,
|
|
254
294
|
`APP_TOKEN_REVOKED_CODE`, `APP_INSTALLATION_GONE_CODE`, `APP_INSTALLATION_PENDING_CODE`,
|
|
255
295
|
`RESYNC_COOLDOWN_CODE`, `TOO_MANY_REQUESTS_CODE`), `AppMintHaltedError`.
|
|
@@ -265,8 +305,9 @@ retries (~4 h) are gone; resync cannot backfill them. Full runbook: `docs/deploy
|
|
|
265
305
|
- **verify** (`signatures.ts`): `verifyQueekSignature` — Standard Webhooks verification (`webhook-id`, `webhook-timestamp`, `webhook-signature` over `{id}.{timestamp}.{body}`, keyed by the decoded `whsec_…` bytes), with timestamp-skew enforcement.
|
|
266
306
|
- **delivery core** (`delivery.ts`): `CoreDelivery` (`rawBody` + `headers`) / `InstallDelivery` (+ `method`/`path`) / `DeliveryResult` (`{ status, body }`) / `CoreHeaders`, plus `readHeader` (case-insensitive, array-tolerant), `decodeBody`, and `toResponse`. Zero request/response types.
|
|
267
307
|
- **install handlers** (`install-handlers.ts`): layer 1 `handleInstallDelivery(input, { appSecret, store, onInstall?, onUninstall?, onSettings? })` serves the signed install/uninstall/settings handoff from raw bytes + headers (routes on the path's trailing segment); layer 2 `handleInstallRequest(request, …)` adapts `Request` → `Response` onto it. Defaults persist the installation (encrypted) in the store; a redelivered install for an existing installation merges idempotently (`saveResyncedInstallation`). The Hono wrapper `createInstallHandlers` lives under `@usequeek/app-sdk/hono` (`hono.ts`).
|
|
268
|
-
- **client** (`client.ts`): `createQueekClient({ apiBase, apiKey })` — the low-level typed fetch client over the Merchant API (`X-Client-Key`), with `Idempotency-Key` on writes, typed `QueekApiError`s, and 429 retry helpers.
|
|
269
|
-
- **webhooks** (`webhooks.ts`): layer 1 `handleWebhookDelivery(input, { store, handlers })` verifies each delivery against the installation's endpoint secret, dedupes on `webhook-id`, and dispatches `topic → handler` at most once; layer 2 `handleWebhookRequest(request, …)` adapts `Request` → `Response` onto it. The Hono wrapper `createWebhookHandler` lives under `@usequeek/app-sdk/hono` (`hono.ts`).
|
|
308
|
+
- **client** (`client.ts`): `createQueekClient({ apiBase, apiKey })` — the low-level typed fetch client over the Merchant API (`X-Client-Key`), with `Idempotency-Key` on writes, typed `QueekApiError`s, and 429 retry helpers. Both it and `createInstallationClient({ installationId, apiBase, tokens })` are generic (`<AppPaths>`, default: the bundled `merchant-schema.ts` compat shim): pass the app's own codegen output for current types with zero SDK publish. The default does NOT auto-update (re-run codegen in the app; sunset signal at 1.0). `openapi/merchant.json` stays in-repo as `gen:merchant`'s reference input and is not shipped in the published package. Prefer `createInstallationClient` in apps.
|
|
309
|
+
- **webhooks** (`webhooks.ts`): layer 1 `handleWebhookDelivery(input, { store, handlers })` verifies each delivery against the installation's endpoint secret, dedupes on `webhook-id`, and dispatches `topic → handler` at most once; layer 2 `handleWebhookRequest(request, …)` adapts `Request` → `Response` onto it. The Hono wrapper `createWebhookHandler` lives under `@usequeek/app-sdk/hono` (`hono.ts`). Unknown topics answer 200 `unhandled` — see Data deletion before relying on that.
|
|
310
|
+
- **proxy** (`proxy.ts`): `verifyProxyQuery` / `verifyProxyQueryDetailed` — app-proxy query verification byte-exact with the backend (`path\nshop\nts\nsorted(k=v&...)`, hex HMAC-SHA256 over the FULL `whsec_…` string, 5-minute skew floored at 60 s, `timingSafeEqual`, previous-secret grace over the secrets list); layer 1 `verifyProxyDelivery(input, { store, … })` resolves the installation from the store by `kid` and claims single-use `jti`; layer 2 `handleProxyRequest(request, { store, path, … }, onVerified)` serves GET only. The Hono wrapper `createProxyHandler` lives under `@usequeek/app-sdk/hono` (`hono.ts`).
|
|
270
311
|
- **store** (`store.ts`): `SqliteInstallationStore` (local/dev/test) and `PostgresInstallationStore`
|
|
271
312
|
(`pg`, pool max 2, advisory-locked schema + `schema_version` row so two containers boot
|
|
272
313
|
safely) — installations encrypted at rest (AES-GCM via `APP_ENCRYPTION_KEY`), plus the
|
|
@@ -282,16 +323,128 @@ retries (~4 h) are gone; resync cannot backfill them. Full runbook: `docs/deploy
|
|
|
282
323
|
PEM private-key blocks — the app JWT and private key can never reach logs.
|
|
283
324
|
- **session** (`session.ts`, server-only via `@usequeek/app-sdk/server`):
|
|
284
325
|
`verifySessionToken` — HS256 dashboard session tokens minted per installation
|
|
285
|
-
(`embsec_…` secret, raw UTF-8 key bytes, 20 s clock tolerance, slug audience
|
|
286
|
-
|
|
326
|
+
(`embsec_…` secret, raw UTF-8 key bytes, 20 s clock tolerance, slug audience,
|
|
327
|
+
issuer = the handoff `apiBase` verbatim (`installation.apiBase` — it equals the
|
|
328
|
+
bare `app.url` the backend signs as `iss`), full installation binding). The secret never enters a
|
|
287
329
|
browser bundle: the main entry does not export the verifier. The install
|
|
288
330
|
and resync handoffs deliver `embed_secret` + `app_id`; the store keeps
|
|
289
331
|
them on the installation (`embedSecret` encrypted, `appId`), and
|
|
290
332
|
`sessionTokenInstallationId` reads the token's `installation_id` as an
|
|
291
333
|
unverified routing hint so a server can load that row before verifying.
|
|
292
|
-
|
|
293
|
-
(
|
|
294
|
-
|
|
334
|
+
`verifyLaunchToken` checks the signed-first-load token the same way plus the purpose pin
|
|
335
|
+
(`purpose` must be `launch`, with the `store` p_id and `theme` hints) — a launch token is refused
|
|
336
|
+
as a bridge token and vice versa.
|
|
337
|
+
- **frame** (`frame.ts`, browser-safe, no secret): the embedded-app bridge
|
|
338
|
+
v1 — typed unions both directions (`ready{capabilities,sdkVersion}`,
|
|
339
|
+
`theme`, `title`/`title-action`, `toast`, `save-bar`/`save-bar-action`,
|
|
340
|
+
`navigate`/`navigated`, `open`, `pick-resource`/`resource-picked`/
|
|
341
|
+
`resource-pick-cancelled`), unknown types ignored, every outbound string
|
|
342
|
+
length-capped client-side. `listenToDashboard` (accepts only the exact
|
|
343
|
+
dashboard origin, plus an `expectSource` window check) and typed senders
|
|
344
|
+
(`sendReady`, `sendToast`, `sendSaveBar`, `sendTitle`, `sendNavigated`,
|
|
345
|
+
`sendOpen`, `sendPickResource`, …) to the exact target origin, never `"*"`.
|
|
346
|
+
`parseOutboundMessage` runs the same per-type validation the dashboard
|
|
347
|
+
runs — the dashboard re-validates everything before acting (open targets,
|
|
348
|
+
title/toast/pick content), so a compromised frame cannot smuggle
|
|
349
|
+
`javascript:` URLs or uncapped strings through the bridge. `open` targets
|
|
350
|
+
are a dashboard-relative path or an absolute `https:` URL; backslashes,
|
|
351
|
+
control characters, protocol-relative URLs and every other scheme are
|
|
352
|
+
refused (by `clipOutbound` as well as `isAllowedOpenTarget`).
|
|
353
|
+
|
|
354
|
+
**Bridge handshake.** The app announces `ready{capabilities,sdkVersion}`;
|
|
355
|
+
the dashboard answers with its `theme` message, which also declares the
|
|
356
|
+
DASHBOARD's capabilities: `{ type: "theme", mode, locale?, capabilities?:
|
|
357
|
+
string[], bridge?: "1" }` (same caps as outbound: at most 32 entries of 64
|
|
358
|
+
characters). `listenToDashboard` exposes both on the `BridgeTheme` passed to
|
|
359
|
+
`onTheme`. A dashboard that omits `capabilities` is legacy (ready/token/
|
|
360
|
+
resize only). `<QueekProvider>` stores the declared set: `pickResource`
|
|
361
|
+
rejects at once when it lacks `pick-resource`, waits up to 1.5 s after
|
|
362
|
+
`ready` when the set is still unknown, and rejects if no handshake arrives.
|
|
363
|
+
Each pick carries a `requestId` (on `pick-resource`); a dashboard that
|
|
364
|
+
declares `pick-resource` MUST echo it on `resource-picked` / `resource-pick-cancelled` — answers with a
|
|
365
|
+
different or missing id are dropped as stale.
|
|
366
|
+
- **auth** (`auth-fetch.ts`, browser-safe, no secret): `installAuthFetch({ exchange })`
|
|
367
|
+
— reads the launch token (`queek_token` query param) from the signed first
|
|
368
|
+
load, strips it with `history.replaceState`, exchanges it once for the
|
|
369
|
+
app's own session, attaches `Authorization: Bearer <session>` to
|
|
370
|
+
same-origin fetch (cookies stay out), and re-establishes via the bridge
|
|
371
|
+
`ready→token` flow + one retry on 401. Session recovery runs only when framed (a post target other than the
|
|
372
|
+
window itself) and is not retried per request after a failed attempt — only
|
|
373
|
+
after a later dashboard token arrives. Pass the same `capabilities`/
|
|
374
|
+
`sdkVersion` as the provider so its refresh `ready` announces one consistent set. The app's
|
|
375
|
+
token-exchange endpoint receives the LAUNCH token on first load and a BRIDGE token on every
|
|
376
|
+
401 refresh through the same `exchange` callback, so it must try
|
|
377
|
+
`verifyLaunchTokenDetailed`/`verifyLaunchToken` first and fall back to `verifySessionToken`
|
|
378
|
+
(both purposes):
|
|
379
|
+
|
|
380
|
+
```ts
|
|
381
|
+
import { verifyLaunchTokenDetailed, verifySessionTokenDetailed } from "@usequeek/app-sdk/server";
|
|
382
|
+
|
|
383
|
+
async function exchange(token: string): Promise<string> {
|
|
384
|
+
const options = { secret: installation.embedSecret!, audience: "my-app", issuer: installation.apiBase, expected };
|
|
385
|
+
const launch = await verifyLaunchTokenDetailed(token, options);
|
|
386
|
+
if (launch.ok) return mintAppSession(launch.claims);
|
|
387
|
+
const bridge = await verifySessionTokenDetailed(token, options);
|
|
388
|
+
if (bridge.ok) return mintAppSession(bridge.claims);
|
|
389
|
+
throw Object.assign(new Error("unauthorized"), { status: 401 });
|
|
390
|
+
}
|
|
391
|
+
```
|
|
392
|
+
- **theme** (`theme.ts`, browser-safe, no secret): `applyTheme(mode)` toggles
|
|
393
|
+
`.dark` on `<html>` + `color-scheme` (shadcn's dark-mode mechanism);
|
|
394
|
+
`themeBootstrapScript()` returns the inline `<head>` snippet that reads
|
|
395
|
+
`theme` from the URL before first paint (no flash);
|
|
396
|
+
`installThemeListener` follows live bridge `theme{mode}` messages (only from
|
|
397
|
+
`window.parent` by default; pass `expectSource` to override). The live mode is
|
|
398
|
+
remembered in `sessionStorage`, and the bootstrap script falls back to it when
|
|
399
|
+
the URL carries no `theme` param, so an in-frame reload never flashes light. Every live `theme` message also
|
|
400
|
+
rewrites the `theme` URL param (other params kept) so a stale first-load param
|
|
401
|
+
never beats the current mode on reload, and the first-load mode is remembered
|
|
402
|
+
too (a legacy dashboard that never sends `theme` still reloads flash-free).
|
|
403
|
+
- **react** (`react.ts`, via `@usequeek/app-sdk/react` — `react` is an
|
|
404
|
+
optional peer, same pattern as `./hono`): `<QueekProvider>` owns one
|
|
405
|
+
bridge subscription; `useQueek()` returns `{ toast, saveBar, title,
|
|
406
|
+
navigate, pickResource, theme }` over the framework-free core.
|
|
407
|
+
- **scopes** (`scopes.ts`): `createInstallationScopesClient({ installationId, store, signJwt, … })`
|
|
408
|
+
— the optional-scopes session, wired to the same store + token provider as the
|
|
409
|
+
installation client. `queryScopes` reads the cached grant (no network);
|
|
410
|
+
`requestScopes` builds the dashboard consent link (pure — the SDK never
|
|
411
|
+
renders consent); `revokeScopes` posts the app-authenticated revoke and
|
|
412
|
+
refreshes the cache. The install/settings handlers accept the signed
|
|
413
|
+
`app/scopes_update` handoff on both routes and refresh the cached grant
|
|
414
|
+
(dropping the cached token when it moved).
|
|
415
|
+
|
|
416
|
+
## Scopes
|
|
417
|
+
|
|
418
|
+
Apps declare required scopes (granted at install) plus optional scopes
|
|
419
|
+
(requested later, revocable). The per-installation session covers the app side:
|
|
420
|
+
|
|
421
|
+
```ts
|
|
422
|
+
import { createInstallationScopesClient } from "@usequeek/app-sdk";
|
|
423
|
+
|
|
424
|
+
const scopes = createInstallationScopesClient({
|
|
425
|
+
installationId,
|
|
426
|
+
store,
|
|
427
|
+
signJwt: () => provider.signJwt(),
|
|
428
|
+
appSlug: "my-app",
|
|
429
|
+
optionalScopes: ["merchant-orders-read"], // the app's own manifest knowledge
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
await scopes.queryScopes(); // { granted, optional } — cached, no network
|
|
433
|
+
scopes.requestScopes(["merchant-orders-read"]); // dashboard consent link (open via sendOpen)
|
|
434
|
+
await scopes.revokeScopes(["merchant-orders-read"]); // 422 app_scope_required → AppScopeRequiredError
|
|
435
|
+
```
|
|
436
|
+
|
|
437
|
+
`queryScopes` splits the cached effective grant against the declared-optional
|
|
438
|
+
list you pass (the app surface exposes the grant but no declared list, so the
|
|
439
|
+
split needs your manifest knowledge). `requestScopes` returns
|
|
440
|
+
`/apps?app={slug}&view=scopes&scopes={a},{b}` (absolute under
|
|
441
|
+
`dashboardOrigin` when configured) — open it via `sendOpen` or a redirect: it
|
|
442
|
+
opens the merchant's consent screen in the dashboard (shipped with dashboard
|
|
443
|
+
item 8). The link carries the slug + the scope list only; the dashboard
|
|
444
|
+
resolves the installation from the signed-in store. The grant reaches you as a
|
|
445
|
+
signed `app/scopes_update` handoff, which the install/settings handlers apply
|
|
446
|
+
like a resync (same verifier, same replay claim): cached grant refreshed,
|
|
447
|
+
cached token dropped only when the grant moved.
|
|
295
448
|
|
|
296
449
|
## Embedded merchant page (S4 stage 2)
|
|
297
450
|
|
|
@@ -307,8 +460,11 @@ Content-Security-Policy: frame-ancestors https://merchant.queek.com
|
|
|
307
460
|
Replace the host with the dashboard origin you registered. `frame-ancestors`
|
|
308
461
|
is a docs-only control the dashboard cannot enforce for you: without it any
|
|
309
462
|
site may frame the page, and the token handshake (origin-bound) is your only
|
|
310
|
-
remaining gate. Verify every token server-side
|
|
311
|
-
|
|
463
|
+
remaining gate. Verify every token server-side before trusting calls that carry one:
|
|
464
|
+
the token-exchange endpoint receives the LAUNCH token on first load and a BRIDGE token on
|
|
465
|
+
every 401 refresh through the same `installAuthFetch({ exchange })` callback, so it must try
|
|
466
|
+
`verifyLaunchTokenDetailed`/`verifyLaunchToken` first and fall back to `verifySessionToken`
|
|
467
|
+
(both purposes — see the `auth` entry above).
|
|
312
468
|
|
|
313
469
|
## Local development
|
|
314
470
|
|
package/dist/app-auth.d.ts
CHANGED
|
@@ -38,6 +38,12 @@ export declare const APP_INSTALLATION_PENDING_CODE = "app_installation_pending";
|
|
|
38
38
|
export declare const RESYNC_COOLDOWN_CODE = "resync_cooldown";
|
|
39
39
|
/** 429 everywhere: honor `Retry-After`, jittered. */
|
|
40
40
|
export declare const TOO_MANY_REQUESTS_CODE = "too_many_requests";
|
|
41
|
+
/**
|
|
42
|
+
* 403 on a merchant call: the cached installation token was minted for an
|
|
43
|
+
* older grant (missing scope). The grant changed server-side — drop the
|
|
44
|
+
* cached token, re-mint once, retry once.
|
|
45
|
+
*/
|
|
46
|
+
export declare const INSUFFICIENT_SCOPE_CODE = "insufficient_scope";
|
|
41
47
|
/** True when a Queek error code is the kill-switch/disabled refusal. */
|
|
42
48
|
export declare function isAppTokenRevoked(code: string | undefined): boolean;
|
|
43
49
|
/** True when a Queek error code is the fatal app-credential refusal. */
|
|
@@ -53,7 +59,7 @@ export declare function isResyncCooldown(code: string | undefined): boolean;
|
|
|
53
59
|
* is dead — drop it, re-mint once, retry once. `api_key_revoked` covers the
|
|
54
60
|
* uninstalled store, the removed kid, and the replaced K-slot; the re-mint
|
|
55
61
|
* then answers 404 `app_installation_gone` (purge) or 401 `invalid_client`
|
|
56
|
-
* (halt) where applicable. Every OTHER merchant 403 (
|
|
62
|
+
* (halt) where applicable. Every OTHER merchant 403 (plan, mode)
|
|
57
63
|
* propagates to the caller without a mint.
|
|
58
64
|
*/
|
|
59
65
|
export declare const API_KEY_REVOKED_CODE = "api_key_revoked";
|
|
@@ -66,6 +72,8 @@ export declare const INVALID_CLIENT_KEY_CODE = "invalid_client_key";
|
|
|
66
72
|
* halt), not a single dead token.
|
|
67
73
|
*/
|
|
68
74
|
export declare function isTokenRefusal(status: number, code: string | undefined): boolean;
|
|
75
|
+
/** True when a merchant-API 403 means the cached token predates a grant change. */
|
|
76
|
+
export declare function isInsufficientScope(status: number, code: string | undefined): boolean;
|
|
69
77
|
/** App-JWT `iat` is backdated this far (clock skew, GitHub's recommendation). */
|
|
70
78
|
export declare const APP_JWT_SKEW_SECONDS = 60;
|
|
71
79
|
/** App-JWT lifetime: `exp = iat + 540`, so `exp − iat` (540 s) stays ≤ the 600 s wire bound. */
|
|
@@ -117,9 +125,22 @@ export interface AppCredentialOptions {
|
|
|
117
125
|
privateKeyPem?: string;
|
|
118
126
|
env?: NodeJS.ProcessEnv;
|
|
119
127
|
}
|
|
128
|
+
/**
|
|
129
|
+
* Accepted `APP_PRIVATE_KEY` forms (env files are one line per variable):
|
|
130
|
+
* raw PEM text, one-line PEM with literal `\n` escapes, or base64 of the
|
|
131
|
+
* full PEM text (one line, no wrapping — what the Developer page shows).
|
|
132
|
+
*/
|
|
133
|
+
export declare const APP_PRIVATE_KEY_FORMS = "raw PEM text, one-line PEM with literal \\n escapes, or base64 of the PEM text";
|
|
134
|
+
/**
|
|
135
|
+
* Normalize an `APP_PRIVATE_KEY` value to PEM text. Detection: a trimmed
|
|
136
|
+
* value starting with `-----BEGIN` is PEM (literal `\n` unescaped first);
|
|
137
|
+
* anything else is base64-decoded and must decode to `-----BEGIN…`.
|
|
138
|
+
* Never echoes the value.
|
|
139
|
+
*/
|
|
140
|
+
export declare function decodePrivateKeyInput(raw: string): string;
|
|
120
141
|
/**
|
|
121
142
|
* Load the app credential: explicit options win, otherwise `APP_SLUG`,
|
|
122
|
-
* `APP_KEY_ID`, `APP_PRIVATE_KEY` from env. The
|
|
143
|
+
* `APP_KEY_ID`, `APP_PRIVATE_KEY` from env. The key must parse as an RSA
|
|
123
144
|
* private key here, once, so a broken key fails at boot — never mid-mint.
|
|
124
145
|
* Throws `InvalidAppCredentialError` before any fetch.
|
|
125
146
|
*/
|
package/dist/app-auth.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"app-auth.d.ts","sourceRoot":"","sources":["../src/app-auth.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;GAWG;AAQH,wFAAwF;AACxF,eAAO,MAAM,mBAAmB,mBAAmB,CAAC;AACpD;;;;;GAKG;AACH,eAAO,MAAM,sBAAsB,sBAAsB,CAAC;AAC1D,+EAA+E;AAC/E,eAAO,MAAM,0BAA0B,0BAA0B,CAAC;AAClE;;;;;;GAMG;AACH,eAAO,MAAM,6BAA6B,6BAA6B,CAAC;AACxE;;;;;GAKG;AACH,eAAO,MAAM,oBAAoB,oBAAoB,CAAC;AACtD,qDAAqD;AACrD,eAAO,MAAM,sBAAsB,sBAAsB,CAAC;
|
|
1
|
+
{"version":3,"file":"app-auth.d.ts","sourceRoot":"","sources":["../src/app-auth.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;GAWG;AAQH,wFAAwF;AACxF,eAAO,MAAM,mBAAmB,mBAAmB,CAAC;AACpD;;;;;GAKG;AACH,eAAO,MAAM,sBAAsB,sBAAsB,CAAC;AAC1D,+EAA+E;AAC/E,eAAO,MAAM,0BAA0B,0BAA0B,CAAC;AAClE;;;;;;GAMG;AACH,eAAO,MAAM,6BAA6B,6BAA6B,CAAC;AACxE;;;;;GAKG;AACH,eAAO,MAAM,oBAAoB,oBAAoB,CAAC;AACtD,qDAAqD;AACrD,eAAO,MAAM,sBAAsB,sBAAsB,CAAC;AAC1D;;;;GAIG;AACH,eAAO,MAAM,uBAAuB,uBAAuB,CAAC;AAE5D,wEAAwE;AACxE,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,GAAG,OAAO,CAEnE;AAED,wEAAwE;AACxE,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,GAAG,OAAO,CAEjE;AAED,+EAA+E;AAC/E,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,GAAG,OAAO,CAEpE;AAED,sGAAsG;AACtG,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,GAAG,OAAO,CAEvE;AAED,8FAA8F;AAC9F,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,GAAG,OAAO,CAElE;AAED;;;;;;;GAOG;AACH,eAAO,MAAM,oBAAoB,oBAAoB,CAAC;AACtD,eAAO,MAAM,oBAAoB,oBAAoB,CAAC;AACtD,eAAO,MAAM,uBAAuB,uBAAuB,CAAC;AAE5D;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,SAAS,GAAG,OAAO,CAMhF;AAED,mFAAmF;AACnF,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,SAAS,GAAG,OAAO,CAErF;AAMD,iFAAiF;AACjF,eAAO,MAAM,oBAAoB,KAAK,CAAC;AACvC,gGAAgG;AAChG,eAAO,MAAM,mBAAmB,MAAM,CAAC;AACvC,oGAAoG;AACpG,eAAO,MAAM,2BAA2B,MAAM,CAAC;AAC/C,yFAAyF;AACzF,eAAO,MAAM,iBAAiB,IAAI,CAAC;AACnC,mGAAmG;AACnG,eAAO,MAAM,oBAAoB,MAAM,CAAC;AACxC,wFAAwF;AACxF,eAAO,MAAM,mBAAmB,OAAQ,CAAC;AACzC,2EAA2E;AAC3E,eAAO,MAAM,mBAAmB,OAAQ,CAAC;AAMzC,0FAA0F;AAC1F,qBAAa,yBAA0B,SAAQ,KAAK;IAClD,QAAQ,CAAC,IAAI,4BAA4B;gBAE7B,OAAO,EAAE,MAAM;CAI5B;AAED;;;;;;;GAOG;AACH,qBAAa,kBAAmB,SAAQ,KAAK;IAC3C,QAAQ,CAAC,IAAI,qBAAqB;IAClC,QAAQ,CAAC,MAAM,EAAE,OAAO,mBAAmB,GAAG,OAAO,sBAAsB,CAAC;gBAEhE,MAAM,EAAE,kBAAkB,CAAC,QAAQ,CAAC,EAAE,MAAM,EAAE,MAAM;CAKjE;AAED,gEAAgE;AAChE,qBAAa,wBAAyB,SAAQ,KAAK;IACjD,QAAQ,CAAC,IAAI,0BAA0B;gBAE3B,cAAc,EAAE,MAAM;CAInC;AAED,MAAM,WAAW,aAAa;IAC5B,+DAA+D;IAC/D,OAAO,EAAE,MAAM,CAAC;IAChB,mFAAmF;IACnF,KAAK,EAAE,MAAM,CAAC;IACd,kGAAkG;IAClG,aAAa,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,oBAAoB;IACnC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;CACzB;AAED;;;;GAIG;AACH,eAAO,MAAM,qBAAqB,mFACgD,CAAC;AAEnF;;;;;GAKG;AACH,wBAAgB,qBAAqB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAYzD;AAED;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,GAAE,oBAAyB,GAAG,aAAa,CAqCnF;AAMD,MAAM,WAAW,iBAAiB;IAChC,UAAU,EAAE,aAAa,CAAC;IAC1B,kFAAkF;IAClF,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED;;;;;GAKG;AACH,wBAAgB,UAAU,CAAC,OAAO,EAAE,iBAAiB,GAAG,MAAM,CAkB7D"}
|
package/dist/app-auth.js
CHANGED
|
@@ -44,6 +44,12 @@ export const APP_INSTALLATION_PENDING_CODE = "app_installation_pending";
|
|
|
44
44
|
export const RESYNC_COOLDOWN_CODE = "resync_cooldown";
|
|
45
45
|
/** 429 everywhere: honor `Retry-After`, jittered. */
|
|
46
46
|
export const TOO_MANY_REQUESTS_CODE = "too_many_requests";
|
|
47
|
+
/**
|
|
48
|
+
* 403 on a merchant call: the cached installation token was minted for an
|
|
49
|
+
* older grant (missing scope). The grant changed server-side — drop the
|
|
50
|
+
* cached token, re-mint once, retry once.
|
|
51
|
+
*/
|
|
52
|
+
export const INSUFFICIENT_SCOPE_CODE = "insufficient_scope";
|
|
47
53
|
/** True when a Queek error code is the kill-switch/disabled refusal. */
|
|
48
54
|
export function isAppTokenRevoked(code) {
|
|
49
55
|
return code === APP_TOKEN_REVOKED_CODE;
|
|
@@ -69,7 +75,7 @@ export function isResyncCooldown(code) {
|
|
|
69
75
|
* is dead — drop it, re-mint once, retry once. `api_key_revoked` covers the
|
|
70
76
|
* uninstalled store, the removed kid, and the replaced K-slot; the re-mint
|
|
71
77
|
* then answers 404 `app_installation_gone` (purge) or 401 `invalid_client`
|
|
72
|
-
* (halt) where applicable. Every OTHER merchant 403 (
|
|
78
|
+
* (halt) where applicable. Every OTHER merchant 403 (plan, mode)
|
|
73
79
|
* propagates to the caller without a mint.
|
|
74
80
|
*/
|
|
75
81
|
export const API_KEY_REVOKED_CODE = "api_key_revoked";
|
|
@@ -87,6 +93,10 @@ export function isTokenRefusal(status, code) {
|
|
|
87
93
|
return (status === 403 &&
|
|
88
94
|
(code === API_KEY_REVOKED_CODE || code === API_KEY_EXPIRED_CODE || code === INVALID_CLIENT_KEY_CODE));
|
|
89
95
|
}
|
|
96
|
+
/** True when a merchant-API 403 means the cached token predates a grant change. */
|
|
97
|
+
export function isInsufficientScope(status, code) {
|
|
98
|
+
return status === 403 && code === INSUFFICIENT_SCOPE_CODE;
|
|
99
|
+
}
|
|
90
100
|
// ---------------------------------------------------------------------------
|
|
91
101
|
// Timing bounds (all in the open, all injectable in tests).
|
|
92
102
|
// ---------------------------------------------------------------------------
|
|
@@ -140,9 +150,32 @@ export class UnknownInstallationError extends Error {
|
|
|
140
150
|
this.name = "UnknownInstallationError";
|
|
141
151
|
}
|
|
142
152
|
}
|
|
153
|
+
/**
|
|
154
|
+
* Accepted `APP_PRIVATE_KEY` forms (env files are one line per variable):
|
|
155
|
+
* raw PEM text, one-line PEM with literal `\n` escapes, or base64 of the
|
|
156
|
+
* full PEM text (one line, no wrapping — what the Developer page shows).
|
|
157
|
+
*/
|
|
158
|
+
export const APP_PRIVATE_KEY_FORMS = "raw PEM text, one-line PEM with literal \\n escapes, or base64 of the PEM text";
|
|
159
|
+
/**
|
|
160
|
+
* Normalize an `APP_PRIVATE_KEY` value to PEM text. Detection: a trimmed
|
|
161
|
+
* value starting with `-----BEGIN` is PEM (literal `\n` unescaped first);
|
|
162
|
+
* anything else is base64-decoded and must decode to `-----BEGIN…`.
|
|
163
|
+
* Never echoes the value.
|
|
164
|
+
*/
|
|
165
|
+
export function decodePrivateKeyInput(raw) {
|
|
166
|
+
const trimmed = raw.trim();
|
|
167
|
+
if (trimmed.startsWith("-----BEGIN")) {
|
|
168
|
+
return trimmed.replace(/\\n/g, "\n").trim();
|
|
169
|
+
}
|
|
170
|
+
const decoded = Buffer.from(trimmed, "base64").toString("utf8");
|
|
171
|
+
if (!decoded.trimStart().startsWith("-----BEGIN")) {
|
|
172
|
+
throw new InvalidAppCredentialError(`Invalid APP_PRIVATE_KEY: not a PEM and not base64 of a PEM. Accepted forms: ${APP_PRIVATE_KEY_FORMS}.`);
|
|
173
|
+
}
|
|
174
|
+
return decoded.trim();
|
|
175
|
+
}
|
|
143
176
|
/**
|
|
144
177
|
* Load the app credential: explicit options win, otherwise `APP_SLUG`,
|
|
145
|
-
* `APP_KEY_ID`, `APP_PRIVATE_KEY` from env. The
|
|
178
|
+
* `APP_KEY_ID`, `APP_PRIVATE_KEY` from env. The key must parse as an RSA
|
|
146
179
|
* private key here, once, so a broken key fails at boot — never mid-mint.
|
|
147
180
|
* Throws `InvalidAppCredentialError` before any fetch.
|
|
148
181
|
*/
|
|
@@ -156,10 +189,11 @@ export function loadAppCredential(options = {}) {
|
|
|
156
189
|
if (keyId === "") {
|
|
157
190
|
throw new InvalidAppCredentialError("Missing APP_KEY_ID: set it to the kid registered with Queek (`php artisan app:register --public-key=…`).");
|
|
158
191
|
}
|
|
159
|
-
const
|
|
160
|
-
if (
|
|
161
|
-
throw new InvalidAppCredentialError(
|
|
192
|
+
const privateKeyInput = (options.privateKeyPem ?? env.APP_PRIVATE_KEY ?? "").trim();
|
|
193
|
+
if (privateKeyInput === "") {
|
|
194
|
+
throw new InvalidAppCredentialError(`Missing APP_PRIVATE_KEY: set it to base64 of the app's RSA private key PEM (one line). Accepted forms: ${APP_PRIVATE_KEY_FORMS}.`);
|
|
162
195
|
}
|
|
196
|
+
const privateKeyPem = decodePrivateKeyInput(privateKeyInput);
|
|
163
197
|
try {
|
|
164
198
|
const key = createPrivateKey(privateKeyPem);
|
|
165
199
|
if (key.asymmetricKeyType !== "rsa") {
|
|
@@ -169,7 +203,7 @@ export function loadAppCredential(options = {}) {
|
|
|
169
203
|
catch (error) {
|
|
170
204
|
if (error instanceof InvalidAppCredentialError)
|
|
171
205
|
throw error;
|
|
172
|
-
throw new InvalidAppCredentialError(`Invalid APP_PRIVATE_KEY: not a parseable PEM private key (${error instanceof Error ? error.message : "undecodable"}).`);
|
|
206
|
+
throw new InvalidAppCredentialError(`Invalid APP_PRIVATE_KEY: not a parseable PEM private key (${error instanceof Error ? error.message : "undecodable"}). Accepted forms: ${APP_PRIVATE_KEY_FORMS}.`);
|
|
173
207
|
}
|
|
174
208
|
// The PEM stays in this object only; callers must never log it (see
|
|
175
209
|
// `signAppJwt`: the only consumer, and it never emits the key).
|
package/dist/app-auth.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"app-auth.js","sourceRoot":"","sources":["../src/app-auth.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAE3D;;;;;;;;;;;GAWG;AAEH,8EAA8E;AAC9E,0EAA0E;AAC1E,2EAA2E;AAC3E,4BAA4B;AAC5B,8EAA8E;AAE9E,wFAAwF;AACxF,MAAM,CAAC,MAAM,mBAAmB,GAAG,gBAAgB,CAAC;AACpD;;;;;GAKG;AACH,MAAM,CAAC,MAAM,sBAAsB,GAAG,mBAAmB,CAAC;AAC1D,+EAA+E;AAC/E,MAAM,CAAC,MAAM,0BAA0B,GAAG,uBAAuB,CAAC;AAClE;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,6BAA6B,GAAG,0BAA0B,CAAC;AACxE;;;;;GAKG;AACH,MAAM,CAAC,MAAM,oBAAoB,GAAG,iBAAiB,CAAC;AACtD,qDAAqD;AACrD,MAAM,CAAC,MAAM,sBAAsB,GAAG,mBAAmB,CAAC;
|
|
1
|
+
{"version":3,"file":"app-auth.js","sourceRoot":"","sources":["../src/app-auth.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Same-origin session carriage for embedded apps (U-validation "Auth
|
|
3
|
+
* carriage"): `installAuthFetch()` reads the launch token from the signed
|
|
4
|
+
* first load, exchanges it ONCE for the app's own session, and attaches
|
|
5
|
+
* `Authorization: Bearer <session>` to every same-origin fetch. A 401
|
|
6
|
+
* re-establishes the session via the bridge `ready→token` flow and retries
|
|
7
|
+
* once. Cookies stay out (`credentials: "omit"` on handled requests —
|
|
8
|
+
* the cross-site iframe cannot rely on them anyway).
|
|
9
|
+
*
|
|
10
|
+
* The caller supplies `exchange(token) => session`: the app's own endpoint
|
|
11
|
+
* that verifies the dashboard token server-side and mints the app
|
|
12
|
+
* session — the exchange runs once on the signed first load so the first
|
|
13
|
+
* paint needs no bridge round-trip, and again (same callback) with a
|
|
14
|
+
* bridge token after every 401 refresh. The endpoint therefore receives
|
|
15
|
+
* the LAUNCH token on first load and a BRIDGE token on every refresh, so
|
|
16
|
+
* it must try `verifyLaunchTokenDetailed`/`verifyLaunchToken` first and
|
|
17
|
+
* fall back to `verifySessionToken` (both purposes). This module never
|
|
18
|
+
* sees secrets — it only carries opaque tokens.
|
|
19
|
+
*
|
|
20
|
+
* DOM-free by design (like `frame.ts`): location/history access is
|
|
21
|
+
* injectable and defaults to the global window when present.
|
|
22
|
+
*/
|
|
23
|
+
import { type EmbedEventTarget, type EmbedPostTarget } from "./frame.js";
|
|
24
|
+
/**
|
|
25
|
+
* Launch token query param on the signed first load. Matches the dashboard
|
|
26
|
+
* frame src contract (`launchFrameUrl` in queek-merchant: the ONLY token
|
|
27
|
+
* that may ride a URL, namespaced so it cannot collide with an app's own
|
|
28
|
+
* `token` param).
|
|
29
|
+
*/
|
|
30
|
+
export declare const LAUNCH_TOKEN_PARAM = "queek_token";
|
|
31
|
+
/** How long to wait for the dashboard's token reply after a `ready`. */
|
|
32
|
+
export declare const BRIDGE_TOKEN_TIMEOUT_MS = 8000;
|
|
33
|
+
export interface AuthFetchOptions {
|
|
34
|
+
/**
|
|
35
|
+
* Exchange a dashboard token for the app's own session (caller's endpoint).
|
|
36
|
+
* The endpoint receives the LAUNCH token on first load and a BRIDGE token
|
|
37
|
+
* on every 401 refresh through this same callback, so it must try
|
|
38
|
+
* `verifyLaunchTokenDetailed`/`verifyLaunchToken` first and fall back to
|
|
39
|
+
* `verifySessionToken` (both purposes).
|
|
40
|
+
*/
|
|
41
|
+
exchange: (token: string) => Promise<string>;
|
|
42
|
+
/** Exact dashboard origin for the `ready→token` refresh flow. */
|
|
43
|
+
dashboardOrigin: string;
|
|
44
|
+
/** Launch-token query param (default `queek_token`). */
|
|
45
|
+
param?: string;
|
|
46
|
+
/** postMessage target for `ready` (defaults to the global window's parent). */
|
|
47
|
+
postTarget?: EmbedPostTarget;
|
|
48
|
+
/**
|
|
49
|
+
* Capabilities announced in the refresh `ready` — pass the SAME list as the
|
|
50
|
+
* React provider / theme listener so the dashboard sees one consistent set.
|
|
51
|
+
*/
|
|
52
|
+
capabilities?: string[];
|
|
53
|
+
/** SDK version announced in the refresh `ready`. */
|
|
54
|
+
sdkVersion?: string;
|
|
55
|
+
/** message-event target for the token reply (defaults to the global window). */
|
|
56
|
+
listenTarget?: EmbedEventTarget;
|
|
57
|
+
/**
|
|
58
|
+
* Required `event.source` for bridge messages. Defaults to the global
|
|
59
|
+
* window's parent when present (the embedding dashboard); pass `null` to
|
|
60
|
+
* disable the source check.
|
|
61
|
+
*/
|
|
62
|
+
expectSource?: unknown;
|
|
63
|
+
/** fetch implementation (defaults to the global fetch). */
|
|
64
|
+
fetchImpl?: typeof fetch;
|
|
65
|
+
/** Current href (defaults to the global window location). */
|
|
66
|
+
getHref?: () => string;
|
|
67
|
+
/** Replace the URL without navigating (defaults to history.replaceState). */
|
|
68
|
+
replaceUrl?: (url: string) => void;
|
|
69
|
+
}
|
|
70
|
+
export interface InstalledAuth {
|
|
71
|
+
/** Session-carrying fetch: same-origin requests gain the session as a Bearer token, and a 401 retries once after a refresh. */
|
|
72
|
+
fetch: (input: string | URL | Request, init?: RequestInit) => Promise<Response>;
|
|
73
|
+
/** Current app session, or null before the exchange settles / without a token. */
|
|
74
|
+
getSession: () => string | null;
|
|
75
|
+
/** Settles to the session, or null when there is no launch token to exchange. */
|
|
76
|
+
ready: Promise<string | null>;
|
|
77
|
+
/** Re-establish the session via the bridge `ready→token` flow. */
|
|
78
|
+
refresh: () => Promise<string>;
|
|
79
|
+
/** Unsubscribe the bridge token listener. */
|
|
80
|
+
dispose: () => void;
|
|
81
|
+
}
|
|
82
|
+
/** Read the launch token from a URL without touching it. */
|
|
83
|
+
export declare function readLaunchToken(href: string, param?: string): string | null;
|
|
84
|
+
/**
|
|
85
|
+
* Strip the launch token from a URL (every other param, plus the hash, is
|
|
86
|
+
* preserved). Returns the stripped href, or null when no token is present.
|
|
87
|
+
*/
|
|
88
|
+
export declare function stripLaunchToken(href: string, param?: string): string | null;
|
|
89
|
+
export declare function installAuthFetch(options: AuthFetchOptions): InstalledAuth;
|
|
90
|
+
//# sourceMappingURL=auth-fetch.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auth-fetch.d.ts","sourceRoot":"","sources":["../src/auth-fetch.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,OAAO,EAAE,KAAK,gBAAgB,EAAE,KAAK,eAAe,EAAgC,MAAM,YAAY,CAAC;AAEvG;;;;;GAKG;AACH,eAAO,MAAM,kBAAkB,gBAAgB,CAAC;AAEhD,wEAAwE;AACxE,eAAO,MAAM,uBAAuB,OAAO,CAAC;AAE5C,MAAM,WAAW,gBAAgB;IAC/B;;;;;;OAMG;IACH,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;IAC7C,iEAAiE;IACjE,eAAe,EAAE,MAAM,CAAC;IACxB,wDAAwD;IACxD,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,+EAA+E;IAC/E,UAAU,CAAC,EAAE,eAAe,CAAC;IAC7B;;;OAGG;IACH,YAAY,CAAC,EAAE,MAAM,EAAE,CAAC;IACxB,oDAAoD;IACpD,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,gFAAgF;IAChF,YAAY,CAAC,EAAE,gBAAgB,CAAC;IAChC;;;;OAIG;IACH,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,2DAA2D;IAC3D,SAAS,CAAC,EAAE,OAAO,KAAK,CAAC;IACzB,6DAA6D;IAC7D,OAAO,CAAC,EAAE,MAAM,MAAM,CAAC;IACvB,6EAA6E;IAC7E,UAAU,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;CACpC;AAED,MAAM,WAAW,aAAa;IAC5B,+HAA+H;IAC/H,KAAK,EAAE,CAAC,KAAK,EAAE,MAAM,GAAG,GAAG,GAAG,OAAO,EAAE,IAAI,CAAC,EAAE,WAAW,KAAK,OAAO,CAAC,QAAQ,CAAC,CAAC;IAChF,kFAAkF;IAClF,UAAU,EAAE,MAAM,MAAM,GAAG,IAAI,CAAC;IAChC,iFAAiF;IACjF,KAAK,EAAE,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IAC9B,kEAAkE;IAClE,OAAO,EAAE,MAAM,OAAO,CAAC,MAAM,CAAC,CAAC;IAC/B,6CAA6C;IAC7C,OAAO,EAAE,MAAM,IAAI,CAAC;CACrB;AAeD,4DAA4D;AAC5D,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,GAAE,MAA2B,GAAG,MAAM,GAAG,IAAI,CAO/F;AAED;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,GAAE,MAA2B,GAAG,MAAM,GAAG,IAAI,CAWhG;AAED,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,gBAAgB,GAAG,aAAa,CAqNzE"}
|