@usequeek/app-sdk 0.6.1 → 0.7.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.
Files changed (107) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +339 -330
  3. package/dist/app-auth.d.ts +15 -17
  4. package/dist/app-auth.js +20 -23
  5. package/dist/auth-fetch.d.ts +14 -5
  6. package/dist/auth-fetch.js +15 -6
  7. package/dist/background.d.ts +10 -2
  8. package/dist/background.js +9 -1
  9. package/dist/browser.d.ts +2 -3
  10. package/dist/browser.js +2 -3
  11. package/dist/client.d.ts +138 -16
  12. package/dist/client.js +200 -43
  13. package/dist/collected.d.ts +13 -15
  14. package/dist/collected.js +4 -5
  15. package/dist/crypto.d.ts +11 -1
  16. package/dist/crypto.js +25 -10
  17. package/dist/delivery.d.ts +22 -9
  18. package/dist/delivery.js +26 -21
  19. package/dist/frame.d.ts +11 -12
  20. package/dist/frame.js +11 -12
  21. package/dist/handoff.d.ts +35 -39
  22. package/dist/handoff.js +9 -10
  23. package/dist/hono.d.ts +3 -4
  24. package/dist/hono.js +3 -4
  25. package/dist/index.d.ts +1 -2
  26. package/dist/index.js +1 -2
  27. package/dist/install-handlers.d.ts +28 -9
  28. package/dist/install-handlers.js +48 -18
  29. package/dist/logger.d.ts +7 -1
  30. package/dist/logger.js +8 -2
  31. package/dist/merchant-schema.d.ts +15678 -6236
  32. package/dist/merchant-schema.js +0 -1
  33. package/dist/proxy.d.ts +21 -24
  34. package/dist/proxy.js +20 -22
  35. package/dist/react.d.ts +10 -2
  36. package/dist/react.js +11 -3
  37. package/dist/resync.d.ts +15 -5
  38. package/dist/resync.js +22 -20
  39. package/dist/scopes.d.ts +36 -27
  40. package/dist/scopes.js +37 -40
  41. package/dist/server.d.ts +0 -1
  42. package/dist/server.js +0 -1
  43. package/dist/session.d.ts +35 -17
  44. package/dist/session.js +32 -15
  45. package/dist/signatures.d.ts +31 -5
  46. package/dist/signatures.js +36 -9
  47. package/dist/store.d.ts +26 -26
  48. package/dist/store.js +16 -20
  49. package/dist/theme.d.ts +1 -2
  50. package/dist/theme.js +1 -2
  51. package/dist/tokens.d.ts +18 -10
  52. package/dist/tokens.js +32 -57
  53. package/dist/webhooks.d.ts +30 -6
  54. package/dist/webhooks.js +27 -10
  55. package/package.json +21 -4
  56. package/dist/app-auth.d.ts.map +0 -1
  57. package/dist/app-auth.js.map +0 -1
  58. package/dist/auth-fetch.d.ts.map +0 -1
  59. package/dist/auth-fetch.js.map +0 -1
  60. package/dist/background.d.ts.map +0 -1
  61. package/dist/background.js.map +0 -1
  62. package/dist/browser.d.ts.map +0 -1
  63. package/dist/browser.js.map +0 -1
  64. package/dist/client.d.ts.map +0 -1
  65. package/dist/client.js.map +0 -1
  66. package/dist/collected.d.ts.map +0 -1
  67. package/dist/collected.js.map +0 -1
  68. package/dist/crypto.d.ts.map +0 -1
  69. package/dist/crypto.js.map +0 -1
  70. package/dist/delivery.d.ts.map +0 -1
  71. package/dist/delivery.js.map +0 -1
  72. package/dist/frame.d.ts.map +0 -1
  73. package/dist/frame.js.map +0 -1
  74. package/dist/handoff.d.ts.map +0 -1
  75. package/dist/handoff.js.map +0 -1
  76. package/dist/hono.d.ts.map +0 -1
  77. package/dist/hono.js.map +0 -1
  78. package/dist/index.d.ts.map +0 -1
  79. package/dist/index.js.map +0 -1
  80. package/dist/install-handlers.d.ts.map +0 -1
  81. package/dist/install-handlers.js.map +0 -1
  82. package/dist/logger.d.ts.map +0 -1
  83. package/dist/logger.js.map +0 -1
  84. package/dist/merchant-schema.d.ts.map +0 -1
  85. package/dist/merchant-schema.js.map +0 -1
  86. package/dist/proxy.d.ts.map +0 -1
  87. package/dist/proxy.js.map +0 -1
  88. package/dist/react.d.ts.map +0 -1
  89. package/dist/react.js.map +0 -1
  90. package/dist/resync.d.ts.map +0 -1
  91. package/dist/resync.js.map +0 -1
  92. package/dist/scopes.d.ts.map +0 -1
  93. package/dist/scopes.js.map +0 -1
  94. package/dist/server.d.ts.map +0 -1
  95. package/dist/server.js.map +0 -1
  96. package/dist/session.d.ts.map +0 -1
  97. package/dist/session.js.map +0 -1
  98. package/dist/signatures.d.ts.map +0 -1
  99. package/dist/signatures.js.map +0 -1
  100. package/dist/store.d.ts.map +0 -1
  101. package/dist/store.js.map +0 -1
  102. package/dist/theme.d.ts.map +0 -1
  103. package/dist/theme.js.map +0 -1
  104. package/dist/tokens.d.ts.map +0 -1
  105. package/dist/tokens.js.map +0 -1
  106. package/dist/webhooks.d.ts.map +0 -1
  107. package/dist/webhooks.js.map +0 -1
package/dist/frame.js CHANGED
@@ -1,14 +1,14 @@
1
1
  /**
2
- * Framed-page bridge client, v1 (U2): the browser-safe half of the
3
- * dashboard bridge. Carries NO secret and performs NO verification — it only
4
- * shapes and addresses messages. The dashboard (queek-merchant) enforces
5
- * origin+source binding and mints tokens; the app backend verifies them via
6
- * `./server` (`verifySessionToken`).
2
+ * Framed-page bridge client, v1: the browser-safe half of the dashboard
3
+ * bridge. Carries NO secret and performs NO verification — it only shapes
4
+ * and addresses messages. The dashboard enforces origin+source binding and
5
+ * mints tokens; the app's server verifies them via `./server`
6
+ * (`verifySessionToken`).
7
7
  *
8
- * Byte-compatible with today's protocol (`ready`/`token`/`resize`/
9
- * `resize-ack`/`ack`, sources `queek-app`/`queek-merchant`, exact-origin
10
- * postMessage): v1 only ADDS optional fields and new message types, and both
11
- * directions ignore unknown types.
8
+ * Compatible with the v0 protocol (`ready`/`token`/`resize`/`resize-ack`/
9
+ * `ack`, sources `queek-app`/`queek-merchant`, exact-origin postMessage):
10
+ * v1 only ADDS optional fields and new message types, and both directions
11
+ * ignore unknown types.
12
12
  *
13
13
  * DOM-free by design (structural minimal types): the app passes its own
14
14
  * window/parent in, so this module needs no DOM lib and stays import-safe
@@ -449,7 +449,7 @@ export function sendReady(dashboardOrigin, target, options) {
449
449
  export function sendResize(dashboardOrigin, target, height) {
450
450
  sendBridgeMessage(dashboardOrigin, target, { source: APP_SOURCE, type: "resize", height });
451
451
  }
452
- /** Delivery confirmation with no state — accepted and ignored by decision. */
452
+ /** Delivery confirmation carrying no state (accepted, no further effect). */
453
453
  export function sendAck(dashboardOrigin, target) {
454
454
  sendBridgeMessage(dashboardOrigin, target, { source: APP_SOURCE, type: "ack" });
455
455
  }
@@ -483,7 +483,7 @@ export function sendToast(dashboardOrigin, target, message, options) {
483
483
  export function sendSaveBar(dashboardOrigin, target, state) {
484
484
  sendBridgeMessage(dashboardOrigin, target, { source: APP_SOURCE, type: "save-bar", state });
485
485
  }
486
- /** Report an in-app move so the dashboard can mirror it in its URL (U2b). */
486
+ /** Report an in-app move so the dashboard can mirror it in its URL. */
487
487
  export function sendNavigated(dashboardOrigin, target, path) {
488
488
  sendBridgeMessage(dashboardOrigin, target, { source: APP_SOURCE, type: "navigated", path });
489
489
  }
@@ -728,4 +728,3 @@ export function clipOutbound(message) {
728
728
  }
729
729
  }
730
730
  }
731
- //# sourceMappingURL=frame.js.map
package/dist/handoff.d.ts CHANGED
@@ -1,26 +1,26 @@
1
1
  /**
2
2
  * Typed payloads for Queek's server-to-server install handoff.
3
3
  *
4
- * Source of truth: queek_backend `App\Services\Apps\AppInstallService`
5
- * (`installPayload()`, `uninstallPayload()`, `settingsPayload()`,
6
- * `handoffBody()`). The signed body envelope is:
4
+ * The signed body envelope is:
7
5
  *
8
6
  * { id, type, api_version: "v1", created_at, data }
9
7
  *
10
8
  * `type` is `app/installed` | `app/uninstalled` | `app/settings_updated`
11
- * | `app/resync` | `app/scopes_update`. The handoff POST carries the Standard Webhooks headers
12
- * (`webhook-id`, `webhook-timestamp`, `webhook-signature`) signed with the
13
- * APP's signing secret (`whsec_…`, minted once at `app:register`), NOT the
9
+ * | `app/resync` | `app/scopes_update`. The handoff POST carries the
10
+ * Standard Webhooks headers (`webhook-id`, `webhook-timestamp`,
11
+ * `webhook-signature`) signed with the APP's signing secret (`whsec_…`,
12
+ * minted when the app is registered on the Queek Developer page), NOT the
14
13
  * per-installation webhook secret. The per-installation `webhook_secret`
15
14
  * arrives INSIDE the install payload (`data.webhook_secret`) — once — so
16
15
  * the app can verify the topic events it is about to receive.
17
16
  *
18
- * `app/resync` is the installation resync handoff (backend
19
- * `AppInstallService::resyncPayload()`, delivered to the app's
20
- * `settings_url` falling back to `install_url`): the install-shaped data
17
+ * `app/resync` is the installation resync handoff (delivered to the app's
18
+ * `settings_url`, falling back to `install_url`): the install-shaped data
21
19
  * plus the resync-only `secret_rotated` flag, over the same signed channel
22
20
  * with the same verification rules. Both the `/install` and `/settings`
23
21
  * handlers accept it.
22
+ *
23
+ * See https://docs.usequeek.com/docs/apps/sdk
24
24
  */
25
25
  export declare const INSTALL_EVENT = "app/installed";
26
26
  export declare const UNINSTALL_EVENT = "app/uninstalled";
@@ -43,45 +43,43 @@ export interface InstallData {
43
43
  /** The Merchant API base for this store (e.g. https://api.usequeek.com/api/v1/merchant). */
44
44
  api_base: string;
45
45
  /**
46
- * No store-callable credential crosses the handoff any more (S1, SDK
47
- * 0.2.0): the app mints short-lived installation tokens with its
48
- * asymmetric app key (`acquireToken()`) and caches them encrypted in its
49
- * own database. The same envelope redelivers `webhook_secret` + the
50
- * non-secret settings snapshot on resync.
46
+ * No store-callable credential crosses the handoff: the app mints
47
+ * short-lived installation tokens with its asymmetric app key
48
+ * (`acquireToken()`) and caches them encrypted in its own database. The
49
+ * same envelope redelivers `webhook_secret` + the non-secret settings
50
+ * snapshot on resync.
51
51
  */
52
52
  scopes: string[];
53
53
  settings: Record<string, unknown>;
54
54
  /** The installation endpoint's `whsec_…` secret, handed over ONCE per rotation. */
55
55
  webhook_secret: string | null;
56
56
  /**
57
- * The installation's `whsec_…` proxy secret (backend `proxy_secret`),
58
- * handed over ONCE per install/resync over the signed channel. The
59
- * booking app signs slot-claims with it; core verifies the HMAC
60
- * against the same installation secret. Null for apps without a
61
- * proxy — the key's absence is the signal. Source of truth:
62
- * queek_backend `App\Services\Apps\AppInstallService::installPayload()`.
57
+ * The installation's `whsec_…` proxy secret, handed over ONCE per
58
+ * install/resync over the signed channel. Queek signs storefront proxy
59
+ * reads with it (see `proxy.ts`), and an app can sign claims back to
60
+ * Queek with it (for example slot-claims); Queek verifies the HMAC
61
+ * against the same installation secret. Null for apps without a proxy —
62
+ * the key's absence is the signal.
63
63
  */
64
64
  proxy_secret: string | null;
65
65
  /**
66
- * The installation's `embsec_…` embed secret (backend `embed_secret`),
67
- * handed over on install/resync: the HS256 key of the dashboard session
68
- * tokens its embedded merchant page receives (verify with
69
- * `@usequeek/app-sdk/server` `verifySessionToken`). Null for apps
70
- * without a merchant page.
66
+ * The installation's `embsec_…` embed secret, handed over on
67
+ * install/resync: the HS256 key of the dashboard session tokens its
68
+ * embedded merchant page receives (verify with `@usequeek/app-sdk/server`
69
+ * `verifySessionToken`). Null for apps without a merchant page.
71
70
  */
72
71
  embed_secret?: string | null;
73
72
  /**
74
73
  * The app's own id, exactly as the session token signs it (`app_id`
75
- * claim) — the verifier binds it. Source of truth: queek_backend
76
- * `AppInstallService::installPayload()` / `resyncPayload()`.
74
+ * claim) — the verifier binds it.
77
75
  */
78
76
  app_id?: string | null;
79
77
  webhook_url: string | null;
80
78
  webhook_topics: string[];
81
79
  /**
82
- * Resync-only (backend `resyncPayload()`): whether the delivery rotated
83
- * the endpoint secret (`webhook_secret` holds the new secret when true,
84
- * null when the installation has no webhook endpoint). Absent on install.
80
+ * Resync-only: whether the delivery rotated the endpoint secret
81
+ * (`webhook_secret` holds the new secret when true, null when the
82
+ * installation has no webhook endpoint). Absent on install.
85
83
  */
86
84
  secret_rotated?: boolean | null;
87
85
  }
@@ -111,20 +109,19 @@ export type SettingsEnvelope = HandoffEnvelope<typeof SETTINGS_EVENT, SettingsDa
111
109
  */
112
110
  export type ResyncEnvelope = HandoffEnvelope<typeof RESYNC_EVENT, InstallData>;
113
111
  /**
114
- * The installation `p_id` the backend addresses a handoff to. The
115
- * scopes_update handoff carries ONLY the `p_id` (never the UUID — the
116
- * backend `p_id` rule), so `id` is optional here: present on
117
- * install-shaped payloads, absent on `app/scopes_update`.
112
+ * The installation `p_id` Queek addresses a handoff to. The
113
+ * scopes_update handoff carries ONLY the `p_id` (never the UUID), so `id`
114
+ * is optional here: present on install-shaped payloads, absent on
115
+ * `app/scopes_update`.
118
116
  */
119
117
  export interface HandoffPidRef {
120
- /** String on the wire (the backend casts); the SDK normalises via `normalisePid`, so a JSON integer still matches. */
118
+ /** A string on the wire; the SDK normalises via `normalisePid`, so a JSON integer still matches. */
121
119
  p_id: string | number;
122
120
  id?: string;
123
121
  }
124
122
  /**
125
- * The grant-change handoff (backend
126
- * `AppInstallService::scopesUpdatePayload()`, queued per grant change over
127
- * the same signed channel): the installation `p_id` + the EFFECTIVE grant
123
+ * The grant-change handoff (sent per grant change over the same signed
124
+ * channel): the installation `p_id` + the EFFECTIVE grant
128
125
  * (grant ∩ tracked manifest — what the installation's tokens now carry,
129
126
  * byte for byte). Carries no settings and no secrets: the app refreshes
130
127
  * its cached grant from `scopes` and drops its cached token when the
@@ -140,4 +137,3 @@ export interface ScopesUpdateData {
140
137
  }
141
138
  export type ScopesUpdateEnvelope = HandoffEnvelope<typeof SCOPES_UPDATE_EVENT, ScopesUpdateData>;
142
139
  export type HandoffEnvelopeAny = InstallEnvelope | UninstallEnvelope | SettingsEnvelope | ResyncEnvelope | ScopesUpdateEnvelope;
143
- //# sourceMappingURL=handoff.d.ts.map
package/dist/handoff.js CHANGED
@@ -1,30 +1,29 @@
1
1
  /**
2
2
  * Typed payloads for Queek's server-to-server install handoff.
3
3
  *
4
- * Source of truth: queek_backend `App\Services\Apps\AppInstallService`
5
- * (`installPayload()`, `uninstallPayload()`, `settingsPayload()`,
6
- * `handoffBody()`). The signed body envelope is:
4
+ * The signed body envelope is:
7
5
  *
8
6
  * { id, type, api_version: "v1", created_at, data }
9
7
  *
10
8
  * `type` is `app/installed` | `app/uninstalled` | `app/settings_updated`
11
- * | `app/resync` | `app/scopes_update`. The handoff POST carries the Standard Webhooks headers
12
- * (`webhook-id`, `webhook-timestamp`, `webhook-signature`) signed with the
13
- * APP's signing secret (`whsec_…`, minted once at `app:register`), NOT the
9
+ * | `app/resync` | `app/scopes_update`. The handoff POST carries the
10
+ * Standard Webhooks headers (`webhook-id`, `webhook-timestamp`,
11
+ * `webhook-signature`) signed with the APP's signing secret (`whsec_…`,
12
+ * minted when the app is registered on the Queek Developer page), NOT the
14
13
  * per-installation webhook secret. The per-installation `webhook_secret`
15
14
  * arrives INSIDE the install payload (`data.webhook_secret`) — once — so
16
15
  * the app can verify the topic events it is about to receive.
17
16
  *
18
- * `app/resync` is the installation resync handoff (backend
19
- * `AppInstallService::resyncPayload()`, delivered to the app's
20
- * `settings_url` falling back to `install_url`): the install-shaped data
17
+ * `app/resync` is the installation resync handoff (delivered to the app's
18
+ * `settings_url`, falling back to `install_url`): the install-shaped data
21
19
  * plus the resync-only `secret_rotated` flag, over the same signed channel
22
20
  * with the same verification rules. Both the `/install` and `/settings`
23
21
  * handlers accept it.
22
+ *
23
+ * See https://docs.usequeek.com/docs/apps/sdk
24
24
  */
25
25
  export const INSTALL_EVENT = "app/installed";
26
26
  export const UNINSTALL_EVENT = "app/uninstalled";
27
27
  export const SETTINGS_EVENT = "app/settings_updated";
28
28
  export const RESYNC_EVENT = "app/resync";
29
29
  export const SCOPES_UPDATE_EVENT = "app/scopes_update";
30
- //# sourceMappingURL=handoff.js.map
package/dist/hono.d.ts CHANGED
@@ -19,14 +19,13 @@ export declare function createInstallHandlers(options: InstallHandlerOptions): H
19
19
  /** Mount the signed topic-delivery receiver (`POST /`). */
20
20
  export declare function createWebhookHandler(options: WebhookHandlerOptions): Hono;
21
21
  /**
22
- * Mount the signed app-proxy reader (`GET`, phase 1 is read-only by binding
23
- * rule) — a thin wrapper built ONLY on the layer-2 Web-standard handler
22
+ * Mount the signed app-proxy reader (`GET` only, proxy reads are
23
+ * read-only) — a thin wrapper built ONLY on the layer-2 Web-standard handler
24
24
  * (`handleProxyRequest` in the root entry). Mount it where the proxy lives
25
25
  * (`app.route("/proxy", createProxyHandler(...))`); `options.path` stays
26
- * the Queek-side canonical path (`/apps/<subpath>/<rest>`) the backend
26
+ * the Queek-side canonical path (`/apps/<subpath>/<rest>`) Queek
27
27
  * signed, and `onVerified` owns the shopper-facing body.
28
28
  */
29
29
  export declare function createProxyHandler(options: ProxyRequestOptions & {
30
30
  onVerified: ProxyResponder;
31
31
  }): Hono;
32
- //# sourceMappingURL=hono.d.ts.map
package/dist/hono.js CHANGED
@@ -26,11 +26,11 @@ export function createWebhookHandler(options) {
26
26
  return app;
27
27
  }
28
28
  /**
29
- * Mount the signed app-proxy reader (`GET`, phase 1 is read-only by binding
30
- * rule) — a thin wrapper built ONLY on the layer-2 Web-standard handler
29
+ * Mount the signed app-proxy reader (`GET` only, proxy reads are
30
+ * read-only) — a thin wrapper built ONLY on the layer-2 Web-standard handler
31
31
  * (`handleProxyRequest` in the root entry). Mount it where the proxy lives
32
32
  * (`app.route("/proxy", createProxyHandler(...))`); `options.path` stays
33
- * the Queek-side canonical path (`/apps/<subpath>/<rest>`) the backend
33
+ * the Queek-side canonical path (`/apps/<subpath>/<rest>`) Queek
34
34
  * signed, and `onVerified` owns the shopper-facing body.
35
35
  */
36
36
  export function createProxyHandler(options) {
@@ -39,4 +39,3 @@ export function createProxyHandler(options) {
39
39
  app.get("*", (c) => handleProxyRequest(c.req.raw, requestOptions, onVerified));
40
40
  return app;
41
41
  }
42
- //# sourceMappingURL=hono.js.map
package/dist/index.d.ts CHANGED
@@ -11,7 +11,7 @@
11
11
  export { API_KEY_EXPIRED_CODE, API_KEY_REVOKED_CODE, APP_INSTALLATION_GONE_CODE, APP_INSTALLATION_PENDING_CODE, APP_JWT_SKEW_SECONDS, APP_JWT_TTL_SECONDS, APP_PRIVATE_KEY_FORMS, APP_TOKEN_REVOKED_CODE, type AppCredential, AppMintHaltedError, decodePrivateKeyInput, INSUFFICIENT_SCOPE_CODE, INVALID_CLIENT_CODE, INVALID_CLIENT_KEY_CODE, InvalidAppCredentialError, isAppTokenRevoked, isInstallationGone, isInstallationPending, isInsufficientScope, isInvalidClient, isResyncCooldown, isTokenRefusal, loadAppCredential, MAX_MINT_ATTEMPTS, MINT_BACKOFF_BASE_MS, MINT_BACKOFF_MAX_MS, RESYNC_COOLDOWN_CODE, RETRY_JITTER_MAX_MS, type SignAppJwtOptions, signAppJwt, TOKEN_VALIDITY_SKEW_SECONDS, TOO_MANY_REQUESTS_CODE, UnknownInstallationError, } from "./app-auth.js";
12
12
  export { type AuthFetchOptions, BRIDGE_TOKEN_TIMEOUT_MS, type InstalledAuth, installAuthFetch, LAUNCH_TOKEN_PARAM, readLaunchToken, stripLaunchToken, } from "./auth-fetch.js";
13
13
  export { backgroundTaskCount, type CatchupFailure, type CatchupInstallation, type CatchupOptions, type CatchupResult, type DetachOptions, detach, runInstallationCatchup, } from "./background.js";
14
- export { apiHostsFromEnv, createQueekClient, DEFAULT_API_HOSTS, devApiHostsFromEnv, InvalidApiBaseError, isWriteMethod, MERCHANT_API_PATH, type MerchantPaths, newIdempotencyKey, type OperationResponse, QueekApiError, type QueekClient, type QueekClientOptions, type QueekErrorDetails, queekApiErrorFromResponse, type RequestOptions, type ResolvedPaths, type RetryOptions, resolveApiBase, type StoreProfile, } from "./client.js";
14
+ export { apiHostsFromEnv, type CursorPage, createQueekClient, DEFAULT_API_HOSTS, DEFAULT_MAX_RETRY_AFTER_MS, DEFAULT_REQUEST_TIMEOUT_MS, devApiHostsFromEnv, InvalidApiBaseError, isWriteMethod, MERCHANT_API_PATH, type MerchantPaths, newIdempotencyKey, type OperationResponse, type PaginateOptions, paginate, QueekApiError, type QueekClient, type QueekClientOptions, type QueekErrorDetails, queekApiErrorFromResponse, type RequestOptions, type ResolvedPaths, type RetryOptions, resolveApiBase, type StoreProfile, } from "./client.js";
15
15
  export { type AlertBody, type AlertSeverity, type CollectedDefinition, type CollectedDefinitionCreate, type CollectedDefinitionUpdate, type CollectedField, collectedDefinitions, createRecord, type MerchantAlert, type SetupNotice, type SetupNoticeBody, type SetupNoticeItem, type SetupNoticeStatus, type SuccessEnvelope, sendAlert, setSetupNotice, } from "./collected.js";
16
16
  export { decryptSecret, encryptSecret, parseStoreKey, storeKeyFingerprint, } from "./crypto.js";
17
17
  export { type CoreDelivery, type CoreHeaders, type DeliveryResult, decodeBody, type InstallDelivery, readHeader, } from "./delivery.js";
@@ -27,4 +27,3 @@ export { createInstallationStore, createPostgresPool, defaultClearCachedTokenIfM
27
27
  export { applyTheme, getThemeModeFromUrl, installThemeListener, rememberThemeMode, THEME_PARAM, THEME_STORAGE_KEY, type ThemeDocument, type ThemeDocumentElement, type ThemeListenerOptions, type ThemeStorage, themeBootstrapScript, } from "./theme.js";
28
28
  export { APP_API_PATH, AppTokenProvider, type AppTokens, createAppTokenProvider, createInstallationClient, type InstallationClientOptions, mintPath, resolveAppApiBase, type TokenProviderOptions, } from "./tokens.js";
29
29
  export { handleWebhookDelivery, handleWebhookRequest, QUEEK_TOPIC_HEADER, type QueekWebhookEnvelope, type SecretResolution, type WebhookHandlerContext, type WebhookHandlerFn, type WebhookHandlerOptions, } from "./webhooks.js";
30
- //# sourceMappingURL=index.d.ts.map
package/dist/index.js CHANGED
@@ -11,7 +11,7 @@
11
11
  export { API_KEY_EXPIRED_CODE, API_KEY_REVOKED_CODE, APP_INSTALLATION_GONE_CODE, APP_INSTALLATION_PENDING_CODE, APP_JWT_SKEW_SECONDS, APP_JWT_TTL_SECONDS, APP_PRIVATE_KEY_FORMS, APP_TOKEN_REVOKED_CODE, AppMintHaltedError, decodePrivateKeyInput, INSUFFICIENT_SCOPE_CODE, INVALID_CLIENT_CODE, INVALID_CLIENT_KEY_CODE, InvalidAppCredentialError, isAppTokenRevoked, isInstallationGone, isInstallationPending, isInsufficientScope, isInvalidClient, isResyncCooldown, isTokenRefusal, loadAppCredential, MAX_MINT_ATTEMPTS, MINT_BACKOFF_BASE_MS, MINT_BACKOFF_MAX_MS, RESYNC_COOLDOWN_CODE, RETRY_JITTER_MAX_MS, signAppJwt, TOKEN_VALIDITY_SKEW_SECONDS, TOO_MANY_REQUESTS_CODE, UnknownInstallationError, } from "./app-auth.js";
12
12
  export { BRIDGE_TOKEN_TIMEOUT_MS, installAuthFetch, LAUNCH_TOKEN_PARAM, readLaunchToken, stripLaunchToken, } from "./auth-fetch.js";
13
13
  export { backgroundTaskCount, detach, runInstallationCatchup, } from "./background.js";
14
- export { apiHostsFromEnv, createQueekClient, DEFAULT_API_HOSTS, devApiHostsFromEnv, InvalidApiBaseError, isWriteMethod, MERCHANT_API_PATH, newIdempotencyKey, QueekApiError, queekApiErrorFromResponse, resolveApiBase, } from "./client.js";
14
+ export { apiHostsFromEnv, createQueekClient, DEFAULT_API_HOSTS, DEFAULT_MAX_RETRY_AFTER_MS, DEFAULT_REQUEST_TIMEOUT_MS, devApiHostsFromEnv, InvalidApiBaseError, isWriteMethod, MERCHANT_API_PATH, newIdempotencyKey, paginate, QueekApiError, queekApiErrorFromResponse, resolveApiBase, } from "./client.js";
15
15
  export { collectedDefinitions, createRecord, sendAlert, setSetupNotice, } from "./collected.js";
16
16
  export { decryptSecret, encryptSecret, parseStoreKey, storeKeyFingerprint, } from "./crypto.js";
17
17
  export { decodeBody, readHeader, } from "./delivery.js";
@@ -27,4 +27,3 @@ export { createInstallationStore, createPostgresPool, defaultClearCachedTokenIfM
27
27
  export { applyTheme, getThemeModeFromUrl, installThemeListener, rememberThemeMode, THEME_PARAM, THEME_STORAGE_KEY, themeBootstrapScript, } from "./theme.js";
28
28
  export { APP_API_PATH, AppTokenProvider, createAppTokenProvider, createInstallationClient, mintPath, resolveAppApiBase, } from "./tokens.js";
29
29
  export { handleWebhookDelivery, handleWebhookRequest, QUEEK_TOPIC_HEADER, } from "./webhooks.js";
30
- //# sourceMappingURL=index.js.map
@@ -1,10 +1,9 @@
1
- import { type DeliveryResult, type InstallDelivery } from "./delivery.js";
1
+ import { type DeliveryErrorHandler, type DeliveryResult, type InstallDelivery } from "./delivery.js";
2
2
  import { type InstallData, type InstallEnvelope, type ScopesUpdateEnvelope, type SettingsEnvelope, type UninstallEnvelope } from "./handoff.js";
3
3
  import type { InstallationRecord, InstallationStore } from "./store.js";
4
4
  /**
5
5
  * Framework-agnostic handlers for Queek's signed server-to-server handoff
6
- * (`AppInstallService::deliver()` in queek_backend) in three thin layers:
7
- * layer 1 `handleInstallDelivery(input, options)` takes plain data
6
+ * in three thin layers: layer 1 `handleInstallDelivery(input, options)` takes plain data
8
7
  * (untouched body bytes + headers, plus method/path for routing) and
9
8
  * returns a plain `{ status, body }` result — zero request/response types,
10
9
  * so whatever request object each framework supports works; layer 2
@@ -13,12 +12,12 @@ import type { InstallationRecord, InstallationStore } from "./store.js";
13
12
  *
14
13
  * Every handoff — install, uninstall, settings, resync — carries the
15
14
  * Standard Webhooks headers signed with the APP signing secret (`whsec_…`,
16
- * minted once at `app:register`). The app answers 2xx only after the
17
- * installation is durably stored; anything else makes Queek revoke the
18
- * just-minted key and mark the install failed (retry = a fresh install).
15
+ * minted when the app is registered on the Queek Developer page). The app
16
+ * answers 2xx only after the installation is durably stored; anything else
17
+ * makes Queek revoke the just-minted key and mark the install failed
18
+ * (retry = a fresh install).
19
19
  *
20
- * The resync handoff (`type: app/resync`, backend
21
- * `AppInstallService::resyncPayload()`) redelivers the install-shaped data
20
+ * The resync handoff (`type: app/resync`) redelivers the install-shaped data
22
21
  * — rotated `webhook_secret`, non-secret settings, and recovery copies of
23
22
  * `proxy_secret` / `embed_secret` / `app_id` — to the app's `settings_url`,
24
23
  * falling back to `install_url`. Both `install` and `settings` accept it
@@ -50,12 +49,18 @@ export interface InstallCallbacks {
50
49
  /** Defaults to refreshing the cached grant (dropping the cached token when it moved). */
51
50
  onScopesUpdate?: (envelope: ScopesUpdateEnvelope) => Promise<void>;
52
51
  }
52
+ /** Options for the install handoff handlers. `appSecret` and `store` are required; every callback has a safe default. */
53
53
  export interface InstallHandlerOptions extends InstallCallbacks {
54
54
  /** The app signing secret (`whsec_…`) from Queek registration. */
55
55
  appSecret: string;
56
56
  store: InstallationStore;
57
57
  nowSeconds?: number;
58
58
  maxSkewSeconds?: number;
59
+ /**
60
+ * Called with the error a callback or store write threw, before the 500
61
+ * goes back. Without it the error is logged to stderr (secrets redacted).
62
+ */
63
+ onError?: DeliveryErrorHandler;
59
64
  }
60
65
  /**
61
66
  * Build the storable installation record from install `data`. Timestamps
@@ -94,12 +99,26 @@ export declare function saveResyncedInstallation(existing: InstallationRecord, d
94
99
  * `uninstall`, or `settings`, taken from the input path's last segment)
95
100
  * from plain data — no `Request`, no framework. Same options, same
96
101
  * behaviour, same errors/status codes at every layer.
102
+ *
103
+ * Answers 401 for a missing header, stale timestamp or bad signature, 400
104
+ * for a body that is not the expected envelope, 409 for a repeated
105
+ * `webhook-id`, 404 for an unknown route, and 500 when your callback or the
106
+ * store write throws (Queek then retries).
107
+ *
108
+ * @example
109
+ * const result = await handleInstallDelivery(
110
+ * { rawBody, headers, method: "POST", path: "/queek/install" },
111
+ * { appSecret: process.env.APP_SIGNING_SECRET ?? "", store },
112
+ * );
113
+ * // reply with result.status and result.body
97
114
  */
98
115
  export declare function handleInstallDelivery(input: InstallDelivery, options: InstallHandlerOptions): Promise<DeliveryResult>;
99
116
  /**
100
117
  * Layer 2: the Web-standard wrapper, built ONLY on layer 1 — reads
101
118
  * `await request.arrayBuffer()` + headers, calls the core, builds the
102
119
  * `Response`.
120
+ *
121
+ * @example
122
+ * export const POST = (request: Request) => handleInstallRequest(request, { appSecret, store });
103
123
  */
104
124
  export declare function handleInstallRequest(request: Request, options: InstallHandlerOptions): Promise<Response>;
105
- //# sourceMappingURL=install-handlers.d.ts.map
@@ -1,4 +1,4 @@
1
- import { decodeBody, readHeader, toResponse, } from "./delivery.js";
1
+ import { decodeBody, readHeader, reportDeliveryFailure, toResponse, } from "./delivery.js";
2
2
  import { INSTALL_EVENT, RESYNC_EVENT, SCOPES_UPDATE_EVENT, SETTINGS_EVENT, UNINSTALL_EVENT, } from "./handoff.js";
3
3
  import { MAX_TIMESTAMP_SKEW_SECONDS, verifyQueekSignatureDetailed, WEBHOOK_ID_HEADER, WEBHOOK_SIGNATURE_HEADER, WEBHOOK_TIMESTAMP_HEADER, } from "./signatures.js";
4
4
  import { normaliseNullablePid, normalisePid } from "./store.js";
@@ -14,8 +14,8 @@ export function buildInstallationRecord(data, nowIso = new Date().toISOString())
14
14
  function installationRecordFromInstall(data, nowIso) {
15
15
  return {
16
16
  installationId: data.installation.id,
17
- // The installation p_id arrives as a string on the wire (the backend
18
- // casts). `normalisePid` stays as tolerance: a JSON integer would reach
17
+ // The installation p_id arrives as a string on the wire.
18
+ // `normalisePid` stays as tolerance: a JSON integer would reach
19
19
  // here as a JS number, which `node:sqlite` would store as `"1021.0"`
20
20
  // in the TEXT column — never equal to the proxy `kid` — so non-string
21
21
  // forms still normalise to integer strings.
@@ -24,7 +24,7 @@ function installationRecordFromInstall(data, nowIso) {
24
24
  storePid: normaliseNullablePid(data.store.p_id),
25
25
  storeName: data.store.name,
26
26
  apiBase: data.api_base,
27
- // No credential crosses the handoff (S1): a fresh row caches no token
27
+ // No credential crosses the handoff: a fresh row caches no token
28
28
  // (the first call mints one); see `saveResyncedInstallation` for the
29
29
  // existing-row path, which keeps the cached token unless the grant
30
30
  // changed.
@@ -106,6 +106,18 @@ export function saveResyncedInstallation(existing, data, nowIso = new Date().toI
106
106
  * `uninstall`, or `settings`, taken from the input path's last segment)
107
107
  * from plain data — no `Request`, no framework. Same options, same
108
108
  * behaviour, same errors/status codes at every layer.
109
+ *
110
+ * Answers 401 for a missing header, stale timestamp or bad signature, 400
111
+ * for a body that is not the expected envelope, 409 for a repeated
112
+ * `webhook-id`, 404 for an unknown route, and 500 when your callback or the
113
+ * store write throws (Queek then retries).
114
+ *
115
+ * @example
116
+ * const result = await handleInstallDelivery(
117
+ * { rawBody, headers, method: "POST", path: "/queek/install" },
118
+ * { appSecret: process.env.APP_SIGNING_SECRET ?? "", store },
119
+ * );
120
+ * // reply with result.status and result.body
109
121
  */
110
122
  export async function handleInstallDelivery(input, options) {
111
123
  if ((input.method ?? "POST").toUpperCase() !== "POST") {
@@ -132,6 +144,9 @@ function trailingSegment(path) {
132
144
  * Layer 2: the Web-standard wrapper, built ONLY on layer 1 — reads
133
145
  * `await request.arrayBuffer()` + headers, calls the core, builds the
134
146
  * `Response`.
147
+ *
148
+ * @example
149
+ * export const POST = (request: Request) => handleInstallRequest(request, { appSecret, store });
135
150
  */
136
151
  export async function handleInstallRequest(request, options) {
137
152
  return toResponse(await handleInstallDelivery({
@@ -169,6 +184,11 @@ function parseEnvelope(rawBody) {
169
184
  return null;
170
185
  }
171
186
  }
187
+ function hasInstallationId(data) {
188
+ return (typeof data === "object" &&
189
+ data !== null &&
190
+ typeof data.installation?.id === "string");
191
+ }
172
192
  function isInstallPayload(data) {
173
193
  return (!!data &&
174
194
  typeof data === "object" &&
@@ -196,14 +216,14 @@ function isScopesUpdatePayload(data) {
196
216
  }
197
217
  /**
198
218
  * Default grant-change apply: find the row by the handoff `p_id` (the
199
- * scopes_update payload carries no UUID — the backend `p_id` rule) and
219
+ * scopes_update payload carries no UUID) and
200
220
  * refresh its cached grant, dropping the cached token when the grant
201
221
  * moved. Unknown `p_id` answers 404 (released, like the settings path).
202
222
  */
203
223
  async function applyScopesUpdate(store, data) {
204
- // The installation p_id arrives as a string on the wire (the backend
205
- // casts) — normalise before comparing anyway, exactly like the install
206
- // mapping does, so a non-conforming integer form still matches.
224
+ // The installation p_id arrives as a string on the wire — normalise
225
+ // before comparing anyway, exactly like the install mapping does, so a
226
+ // non-conforming integer form still matches.
207
227
  const pid = normalisePid(data.installation.p_id);
208
228
  const rows = await store.listInstallations();
209
229
  const existing = rows.find((row) => row.installationPid === pid) ?? null;
@@ -243,8 +263,9 @@ async function scopesDelivery(rawBody, headers, options) {
243
263
  }
244
264
  }
245
265
  }
246
- catch {
266
+ catch (error) {
247
267
  await options.store.releaseWebhookId(checked.id);
268
+ reportDeliveryFailure(options.onError, error, { stage: "scopes_update", eventId: checked.id });
248
269
  return { status: 500, body: { ok: false, error: "scopes_update_failed" } };
249
270
  }
250
271
  return { status: 200, body: { ok: true } };
@@ -256,8 +277,8 @@ async function installDelivery(rawBody, headers, options) {
256
277
  const envelope = parseEnvelope(rawBody);
257
278
  if (!envelope)
258
279
  return { status: 400, body: { ok: false, error: "invalid_envelope" } };
259
- // The grant-change handoff (`type: app/scopes_update`, backend
260
- // `scopesUpdatePayload()`) is delivered to the settings URL first,
280
+ // The grant-change handoff (`type: app/scopes_update`) is delivered to
281
+ // the settings URL first,
261
282
  // falling back to the install URL — both routes accept it with the same
262
283
  // verification, claim, and callback-failure rules as every handoff.
263
284
  if (envelope.type === SCOPES_UPDATE_EVENT) {
@@ -265,8 +286,8 @@ async function installDelivery(rawBody, headers, options) {
265
286
  }
266
287
  // The platform resync handoff redelivers the install-shaped data under
267
288
  // `type: app/resync` (same signed channel, same verification): it lands
268
- // here as well as on `settings` (the backend aims at `settings_url`
269
- // first, falling back to `install_url`).
289
+ // here as well as on `settings` (Queek aims at `settings_url` first,
290
+ // falling back to `install_url`).
270
291
  if (envelope.type !== INSTALL_EVENT && envelope.type !== RESYNC_EVENT) {
271
292
  return { status: 400, body: { ok: false, error: "unexpected event type" } };
272
293
  }
@@ -291,10 +312,11 @@ async function installDelivery(rawBody, headers, options) {
291
312
  await applyResync(options.store, envelope.data);
292
313
  }
293
314
  }
294
- catch {
315
+ catch (error) {
295
316
  // Non-2xx on purpose: Queek revokes the key and the merchant retries
296
317
  // as a fresh install. The claim is released so the retry can land.
297
318
  await options.store.releaseWebhookId(checked.id);
319
+ reportDeliveryFailure(options.onError, error, { stage: "install", eventId: checked.id });
298
320
  return { status: 500, body: { ok: false, error: "install_failed" } };
299
321
  }
300
322
  return { status: 200, body: { ok: true } };
@@ -309,6 +331,9 @@ async function uninstallDelivery(rawBody, headers, options) {
309
331
  if (envelope.type !== UNINSTALL_EVENT) {
310
332
  return { status: 400, body: { ok: false, error: "unexpected event type" } };
311
333
  }
334
+ if (!hasInstallationId(envelope.data)) {
335
+ return { status: 400, body: { ok: false, error: "invalid uninstall payload" } };
336
+ }
312
337
  if (!(await options.store.claimWebhookId(checked.id))) {
313
338
  return { status: 409, body: { ok: false, error: "duplicate delivery" } };
314
339
  }
@@ -320,8 +345,9 @@ async function uninstallDelivery(rawBody, headers, options) {
320
345
  await options.store.deleteInstallation(envelope.data.installation.id);
321
346
  }
322
347
  }
323
- catch {
348
+ catch (error) {
324
349
  await options.store.releaseWebhookId(checked.id);
350
+ reportDeliveryFailure(options.onError, error, { stage: "uninstall", eventId: checked.id });
325
351
  return { status: 500, body: { ok: false, error: "uninstall_failed" } };
326
352
  }
327
353
  return { status: 200, body: { ok: true } };
@@ -355,12 +381,16 @@ async function settingsDelivery(rawBody, headers, options) {
355
381
  try {
356
382
  await applyResync(options.store, envelope.data);
357
383
  }
358
- catch {
384
+ catch (error) {
359
385
  await options.store.releaseWebhookId(checked.id);
386
+ reportDeliveryFailure(options.onError, error, { stage: "settings", eventId: checked.id });
360
387
  return { status: 500, body: { ok: false, error: "settings_failed" } };
361
388
  }
362
389
  return { status: 200, body: { ok: true } };
363
390
  }
391
+ if (!hasInstallationId(envelope.data)) {
392
+ return { status: 400, body: { ok: false, error: "invalid settings payload" } };
393
+ }
364
394
  if (!(await options.store.claimWebhookId(checked.id))) {
365
395
  return { status: 409, body: { ok: false, error: "duplicate delivery" } };
366
396
  }
@@ -382,10 +412,10 @@ async function settingsDelivery(rawBody, headers, options) {
382
412
  });
383
413
  }
384
414
  }
385
- catch {
415
+ catch (error) {
386
416
  await options.store.releaseWebhookId(checked.id);
417
+ reportDeliveryFailure(options.onError, error, { stage: "settings", eventId: checked.id });
387
418
  return { status: 500, body: { ok: false, error: "settings_failed" } };
388
419
  }
389
420
  return { status: 200, body: { ok: true } };
390
421
  }
391
- //# sourceMappingURL=install-handlers.js.map
package/dist/logger.d.ts CHANGED
@@ -23,5 +23,11 @@ export interface Logger {
23
23
  warn(message: string, fields?: LogFields): void;
24
24
  error(message: string, fields?: LogFields): void;
25
25
  }
26
+ /**
27
+ * A JSON-lines logger with secret redaction built in.
28
+ *
29
+ * @example
30
+ * const logger = createLogger({ service: "my-app" });
31
+ * logger.info("order synced", { order: "ord_123" });
32
+ */
26
33
  export declare function createLogger(options: LoggerOptions): Logger;
27
- //# sourceMappingURL=logger.d.ts.map
package/dist/logger.js CHANGED
@@ -12,7 +12,7 @@
12
12
  const LEVELS = { debug: 0, info: 1, warn: 2, error: 3 };
13
13
  const SENSITIVE_KEY_RE = /key|secret|token|passwd|password|auth|credential|session/i;
14
14
  // Long opaque values Queek and friends mint: sk_live_/sk_test_, pk_*, whsec_*, Bearer …
15
- // — plus the S1 app credential shapes, which must NEVER reach logs: a whole PEM
15
+ // — plus the app credential shapes, which must NEVER reach logs: a whole PEM
16
16
  // private-key block and a bare RS256 JWT (eyJ….….…, with or without a Bearer prefix).
17
17
  const SECRET_VALUE_RE = /-----BEGIN [A-Z0-9 ]*PRIVATE KEY-----[\s\S]*?-----END [A-Z0-9 ]*PRIVATE KEY-----|\b(eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+|sk_(live|test)_[A-Za-z0-9_-]+|pk_(live|test)_[A-Za-z0-9_-]+|whsec_[A-Za-z0-9+/=_-]+|Bearer\s+[A-Za-z0-9._~+/-]+=*)\b/g;
18
18
  export const REDACTED = "[redacted]";
@@ -40,6 +40,13 @@ function redactUnknown(value, depth) {
40
40
  }
41
41
  return value;
42
42
  }
43
+ /**
44
+ * A JSON-lines logger with secret redaction built in.
45
+ *
46
+ * @example
47
+ * const logger = createLogger({ service: "my-app" });
48
+ * logger.info("order synced", { order: "ord_123" });
49
+ */
43
50
  export function createLogger(options) {
44
51
  const threshold = LEVELS[options.level ?? process.env.LOG_LEVEL ?? "info"] ?? 1;
45
52
  const sink = options.sink ??
@@ -72,4 +79,3 @@ export function createLogger(options) {
72
79
  error: (message, fields) => emit("error", message, fields),
73
80
  };
74
81
  }
75
- //# sourceMappingURL=logger.js.map