@usequeek/app-sdk 0.4.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 (77) hide show
  1. package/CHANGELOG.md +120 -0
  2. package/LICENSE +21 -0
  3. package/README.md +210 -0
  4. package/dist/app-auth.d.ts +139 -0
  5. package/dist/app-auth.d.ts.map +1 -0
  6. package/dist/app-auth.js +205 -0
  7. package/dist/app-auth.js.map +1 -0
  8. package/dist/background.d.ts +68 -0
  9. package/dist/background.d.ts.map +1 -0
  10. package/dist/background.js +78 -0
  11. package/dist/background.js.map +1 -0
  12. package/dist/client.d.ts +148 -0
  13. package/dist/client.d.ts.map +1 -0
  14. package/dist/client.js +269 -0
  15. package/dist/client.js.map +1 -0
  16. package/dist/collected.d.ts +103 -0
  17. package/dist/collected.d.ts.map +1 -0
  18. package/dist/collected.js +59 -0
  19. package/dist/collected.js.map +1 -0
  20. package/dist/crypto.d.ts +6 -0
  21. package/dist/crypto.d.ts.map +1 -0
  22. package/dist/crypto.js +76 -0
  23. package/dist/crypto.js.map +1 -0
  24. package/dist/frame.d.ts +58 -0
  25. package/dist/frame.d.ts.map +1 -0
  26. package/dist/frame.js +56 -0
  27. package/dist/frame.js.map +1 -0
  28. package/dist/handoff.d.ts +113 -0
  29. package/dist/handoff.d.ts.map +1 -0
  30. package/dist/handoff.js +29 -0
  31. package/dist/handoff.js.map +1 -0
  32. package/dist/index.d.ts +25 -0
  33. package/dist/index.d.ts.map +1 -0
  34. package/dist/index.js +25 -0
  35. package/dist/index.js.map +1 -0
  36. package/dist/install-handlers.d.ts +62 -0
  37. package/dist/install-handlers.d.ts.map +1 -0
  38. package/dist/install-handlers.js +238 -0
  39. package/dist/install-handlers.js.map +1 -0
  40. package/dist/logger.d.ts +27 -0
  41. package/dist/logger.d.ts.map +1 -0
  42. package/dist/logger.js +75 -0
  43. package/dist/logger.js.map +1 -0
  44. package/dist/merchant-schema.d.ts +13963 -0
  45. package/dist/merchant-schema.d.ts.map +1 -0
  46. package/dist/merchant-schema.js +11 -0
  47. package/dist/merchant-schema.js.map +1 -0
  48. package/dist/resync.d.ts +80 -0
  49. package/dist/resync.d.ts.map +1 -0
  50. package/dist/resync.js +203 -0
  51. package/dist/resync.js.map +1 -0
  52. package/dist/server.d.ts +9 -0
  53. package/dist/server.d.ts.map +1 -0
  54. package/dist/server.js +8 -0
  55. package/dist/server.js.map +1 -0
  56. package/dist/session.d.ts +66 -0
  57. package/dist/session.d.ts.map +1 -0
  58. package/dist/session.js +155 -0
  59. package/dist/session.js.map +1 -0
  60. package/dist/signatures.d.ts +55 -0
  61. package/dist/signatures.d.ts.map +1 -0
  62. package/dist/signatures.js +74 -0
  63. package/dist/signatures.js.map +1 -0
  64. package/dist/store.d.ts +278 -0
  65. package/dist/store.d.ts.map +1 -0
  66. package/dist/store.js +610 -0
  67. package/dist/store.js.map +1 -0
  68. package/dist/tokens.d.ts +162 -0
  69. package/dist/tokens.d.ts.map +1 -0
  70. package/dist/tokens.js +411 -0
  71. package/dist/tokens.js.map +1 -0
  72. package/dist/webhooks.d.ts +60 -0
  73. package/dist/webhooks.d.ts.map +1 -0
  74. package/dist/webhooks.js +125 -0
  75. package/dist/webhooks.js.map +1 -0
  76. package/openapi/merchant.json +21452 -0
  77. package/package.json +63 -0
@@ -0,0 +1,162 @@
1
+ import { type AppCredential } from "./app-auth.js";
2
+ import { QueekApiError, type QueekClient } from "./client.js";
3
+ import { type Logger } from "./logger.js";
4
+ import type { InstallationStore } from "./store.js";
5
+ /**
6
+ * Installation tokens (S1, SDK 0.2.0): the app holds ONE asymmetric app
7
+ * credential and mints short-lived installation tokens from Queek on
8
+ * demand, cached (encrypted) in its own database to near-expiry.
9
+ *
10
+ * - `acquireToken(installationId)`: cached-valid token (expiry more than 5
11
+ * minutes away) → else sign an app JWT → `POST access_tokens` → persist
12
+ * (encrypted, expires_at, kid) → return. Every Queek API call in the
13
+ * installation client goes through it.
14
+ * - A merchant-API token refusal → drop it, re-mint once, retry once; a
15
+ * second refusal propagates. Token refusal = any 401, or a 403 with
16
+ * `api_key_revoked` / `api_key_expired` / `invalid_client_key` (rev 7).
17
+ * A merchant 403 `app_token_revoked` is the app-wide kill switch (drop
18
+ * all + halt, no mint); any other merchant 403 (scope, plan, mode)
19
+ * propagates untouched. (If the re-mint itself hits `invalid_client` /
20
+ * kill-switch / gone, the mint path below applies those rules.)
21
+ * - Concurrent callers in one process share ONE in-flight mint per
22
+ * installation (single-flight). Across containers two mints are harmless
23
+ * by design: Queek keeps coexisting tokens valid (K=2 slots) and the
24
+ * last write wins the shared cache row; a residual race self-heals via
25
+ * re-mint on 401.
26
+ * - Error handling exactly per the wire contract: 401 `invalid_client`
27
+ * halts minting app-wide (loud log, no retry loop); 403 kill-switch
28
+ * drops ALL cached tokens and halts; 404 `app_installation_gone` purges
29
+ * the installation; 409 `app_installation_pending` retries later with
30
+ * backoff — NEVER purges, NEVER halts (the persisted pending mark keeps
31
+ * the row through restarts and resync's purge-absent step); 429 honors
32
+ * `Retry-After` +
33
+ * jitter with bounded retries; 5xx/network bounded exponential backoff.
34
+ */
35
+ /** Path of the app-credential API below the store host. */
36
+ export declare const APP_API_PATH = "/api/v1/apps";
37
+ /** Minimal surface the installation client needs (stubs stay one-liners). */
38
+ export interface AppTokens {
39
+ acquireToken(installationId: string): Promise<string>;
40
+ dropCachedToken(installationId: string): Promise<void>;
41
+ /**
42
+ * Kill-switch path: forget EVERY cached token for the app and halt
43
+ * minting (loud log inside). Called on a merchant 403 `app_token_revoked`.
44
+ */
45
+ revokeAppAccess(): Promise<void>;
46
+ }
47
+ export interface TokenProviderOptions {
48
+ credential: AppCredential;
49
+ store: InstallationStore;
50
+ fetchImpl?: typeof fetch;
51
+ /** Sent as User-Agent on app-API calls. Defaults to `queek-app/1.0`. */
52
+ userAgent?: string;
53
+ /** Extra allowed `apiBase` hosts (test/local backends). */
54
+ allowedApiHosts?: string[];
55
+ /** Loud halt/drop logs go here. Defaults to a `queek-app-tokens` logger (stderr on warn+). */
56
+ logger?: Logger;
57
+ /** Injectable clock (ms since epoch). Defaults to `Date.now`. */
58
+ nowMs?: () => number;
59
+ /** Injectable sleep for tests. Defaults to a real setTimeout sleep. */
60
+ sleep?: (ms: number) => Promise<void>;
61
+ /** Injectable uniform [0,1) for jitter. Defaults to `Math.random`. */
62
+ random?: () => number;
63
+ }
64
+ /**
65
+ * Validate the handoff `apiBase` (https + allowlist, same rules as the
66
+ * merchant client) and normalize it to the app-API base
67
+ * (`<origin>/api/v1/apps`). Throws `InvalidApiBaseError` before any fetch.
68
+ */
69
+ export declare function resolveAppApiBase(raw: string, allowedApiHosts?: string[]): string;
70
+ export declare function mintPath(installationId: string): string;
71
+ export declare class AppTokenProvider implements AppTokens {
72
+ private readonly credential;
73
+ private readonly store;
74
+ private readonly fetchImpl;
75
+ private readonly userAgent;
76
+ private readonly allowedApiHosts;
77
+ private readonly logger;
78
+ private readonly nowMs;
79
+ private readonly sleep;
80
+ private readonly random;
81
+ private readonly inflight;
82
+ private halted;
83
+ constructor(options: TokenProviderOptions);
84
+ /** True while minting is halted app-wide (`invalid_client` or kill switch). */
85
+ isHalted(): boolean;
86
+ /**
87
+ * Clear a halt (e.g. the key was fixed / the app re-enabled). A process
88
+ * restart clears it too — the halt is in-process only.
89
+ */
90
+ resumeMinting(): void;
91
+ acquireToken(installationId: string): Promise<string>;
92
+ private acquireUnshared;
93
+ private isFresh;
94
+ dropCachedToken(installationId: string): Promise<void>;
95
+ /** Kill-switch path (also used by ops): forget every cached token, keep the rows. */
96
+ dropAllCachedTokens(): Promise<void>;
97
+ /**
98
+ * Persist the 409 `app_installation_pending` mark for this installation
99
+ * (store column — survives restarts, B2 review r2). Resync's purge-absent
100
+ * step keeps marked rows (the list covers active installations only).
101
+ * Called by the mint path and by `resyncFromQueek`; idempotent, and a
102
+ * no-op when the row is already gone.
103
+ */
104
+ markInstallationPending(installationId: string): Promise<void>;
105
+ /**
106
+ * Clear the persisted pending mark: the installation answered active
107
+ * again (mint success, resync 202/redelivery).
108
+ */
109
+ clearInstallationPending(installationId: string): Promise<void>;
110
+ /**
111
+ * True when the row carries the persisted pending mark. Consulted by
112
+ * resync's purge-absent step; false when the row is missing.
113
+ */
114
+ isKnownPending(installationId: string): Promise<boolean>;
115
+ /**
116
+ * Kill-switch path from any surface (mint 403, merchant 403, ops):
117
+ * forget EVERY cached token for the app and halt minting, loudly.
118
+ * Clears on process restart or `resumeMinting()`.
119
+ */
120
+ revokeAppAccess(): Promise<void>;
121
+ /** Sign a fresh app JWT (exposed for the app-API calls in `resync.ts`). */
122
+ signJwt(nowSeconds?: number): string;
123
+ /**
124
+ * Map an app-endpoint failure to its contract behavior. ALWAYS throws:
125
+ * the halted error, or the original `QueekApiError` after performing the
126
+ * side effect (drop-all / purge). `installationPid` is log-only.
127
+ */
128
+ handleAppEndpointError(error: QueekApiError, installation: {
129
+ id: string;
130
+ pid: string;
131
+ }): Promise<never>;
132
+ private mintWithRetry;
133
+ private jittered;
134
+ private mintOnce;
135
+ }
136
+ /** Create a provider in one call (thin wrapper over `new AppTokenProvider`). */
137
+ export declare function createAppTokenProvider(options: TokenProviderOptions): AppTokenProvider;
138
+ export interface InstallationClientOptions {
139
+ installationId: string;
140
+ /** Handoff `api_base` (bare store host or full merchant base). Validated at construction. */
141
+ apiBase: string;
142
+ tokens: AppTokens;
143
+ fetchImpl?: typeof fetch;
144
+ /** Sent as User-Agent. Defaults to `queek-app/1.0`. */
145
+ userAgent?: string;
146
+ /** Extra allowed `apiBase` hosts (test/local backends). */
147
+ allowedApiHosts?: string[];
148
+ }
149
+ /**
150
+ * A `QueekClient` whose every call goes through `acquireToken()`: the
151
+ * installation token is resolved per request (usually a cache hit — the
152
+ * store IS the cache, one shared row per installation), sent as
153
+ * `X-Client-Key`, and on a merchant-API token refusal (any 401, or 403
154
+ * `api_key_revoked` / `api_key_expired` / `invalid_client_key`) it is
155
+ * dropped, re-minted once, and the call retried once; a second refusal
156
+ * propagates to the caller. A merchant 403 `app_token_revoked` drops all
157
+ * cached tokens and halts minting; any other 403 propagates without a
158
+ * mint. 429/network retries (`requestWithRetry`) reuse one idempotency key
159
+ * exactly like the static client.
160
+ */
161
+ export declare function createInstallationClient(options: InstallationClientOptions): QueekClient;
162
+ //# sourceMappingURL=tokens.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tokens.d.ts","sourceRoot":"","sources":["../src/tokens.ts"],"names":[],"mappings":"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"}
package/dist/tokens.js ADDED
@@ -0,0 +1,411 @@
1
+ import { APP_TOKEN_REVOKED_CODE, AppMintHaltedError, INVALID_CLIENT_CODE, isAppTokenRevoked, isInstallationGone, isInstallationPending, isTokenRefusal, MAX_MINT_ATTEMPTS, MINT_BACKOFF_BASE_MS, MINT_BACKOFF_MAX_MS, RETRY_JITTER_MAX_MS, signAppJwt, TOKEN_VALIDITY_SKEW_SECONDS, UnknownInstallationError, } from "./app-auth.js";
2
+ import { createQueekClient, MERCHANT_API_PATH, QueekApiError, queekApiErrorFromResponse, resolveApiBase, } from "./client.js";
3
+ import { createLogger } from "./logger.js";
4
+ /**
5
+ * Installation tokens (S1, SDK 0.2.0): the app holds ONE asymmetric app
6
+ * credential and mints short-lived installation tokens from Queek on
7
+ * demand, cached (encrypted) in its own database to near-expiry.
8
+ *
9
+ * - `acquireToken(installationId)`: cached-valid token (expiry more than 5
10
+ * minutes away) → else sign an app JWT → `POST access_tokens` → persist
11
+ * (encrypted, expires_at, kid) → return. Every Queek API call in the
12
+ * installation client goes through it.
13
+ * - A merchant-API token refusal → drop it, re-mint once, retry once; a
14
+ * second refusal propagates. Token refusal = any 401, or a 403 with
15
+ * `api_key_revoked` / `api_key_expired` / `invalid_client_key` (rev 7).
16
+ * A merchant 403 `app_token_revoked` is the app-wide kill switch (drop
17
+ * all + halt, no mint); any other merchant 403 (scope, plan, mode)
18
+ * propagates untouched. (If the re-mint itself hits `invalid_client` /
19
+ * kill-switch / gone, the mint path below applies those rules.)
20
+ * - Concurrent callers in one process share ONE in-flight mint per
21
+ * installation (single-flight). Across containers two mints are harmless
22
+ * by design: Queek keeps coexisting tokens valid (K=2 slots) and the
23
+ * last write wins the shared cache row; a residual race self-heals via
24
+ * re-mint on 401.
25
+ * - Error handling exactly per the wire contract: 401 `invalid_client`
26
+ * halts minting app-wide (loud log, no retry loop); 403 kill-switch
27
+ * drops ALL cached tokens and halts; 404 `app_installation_gone` purges
28
+ * the installation; 409 `app_installation_pending` retries later with
29
+ * backoff — NEVER purges, NEVER halts (the persisted pending mark keeps
30
+ * the row through restarts and resync's purge-absent step); 429 honors
31
+ * `Retry-After` +
32
+ * jitter with bounded retries; 5xx/network bounded exponential backoff.
33
+ */
34
+ /** Path of the app-credential API below the store host. */
35
+ export const APP_API_PATH = "/api/v1/apps";
36
+ function defaultSleep(ms) {
37
+ return new Promise((resolve) => setTimeout(resolve, ms));
38
+ }
39
+ /**
40
+ * Validate the handoff `apiBase` (https + allowlist, same rules as the
41
+ * merchant client) and normalize it to the app-API base
42
+ * (`<origin>/api/v1/apps`). Throws `InvalidApiBaseError` before any fetch.
43
+ */
44
+ export function resolveAppApiBase(raw, allowedApiHosts = []) {
45
+ const merchant = resolveApiBase(raw, allowedApiHosts);
46
+ return `${merchant.slice(0, merchant.length - MERCHANT_API_PATH.length)}${APP_API_PATH}`;
47
+ }
48
+ export function mintPath(installationId) {
49
+ return `/installations/${encodeURIComponent(installationId)}/access_tokens`;
50
+ }
51
+ export class AppTokenProvider {
52
+ credential;
53
+ store;
54
+ fetchImpl;
55
+ userAgent;
56
+ allowedApiHosts;
57
+ logger;
58
+ nowMs;
59
+ sleep;
60
+ random;
61
+ inflight = new Map();
62
+ halted = null;
63
+ constructor(options) {
64
+ this.credential = options.credential;
65
+ this.store = options.store;
66
+ this.fetchImpl = options.fetchImpl ?? fetch;
67
+ this.userAgent = options.userAgent ?? "queek-app/1.0";
68
+ this.allowedApiHosts = options.allowedApiHosts ?? [];
69
+ this.logger = options.logger ?? createLogger({ service: "queek-app-tokens" });
70
+ this.nowMs = options.nowMs ?? Date.now;
71
+ this.sleep = options.sleep ?? defaultSleep;
72
+ this.random = options.random ?? Math.random;
73
+ }
74
+ /** True while minting is halted app-wide (`invalid_client` or kill switch). */
75
+ isHalted() {
76
+ return this.halted !== null;
77
+ }
78
+ /**
79
+ * Clear a halt (e.g. the key was fixed / the app re-enabled). A process
80
+ * restart clears it too — the halt is in-process only.
81
+ */
82
+ resumeMinting() {
83
+ this.halted = null;
84
+ }
85
+ async acquireToken(installationId) {
86
+ const shared = this.inflight.get(installationId);
87
+ if (shared)
88
+ return shared;
89
+ const attempt = this.acquireUnshared(installationId);
90
+ this.inflight.set(installationId, attempt);
91
+ try {
92
+ return await attempt;
93
+ }
94
+ finally {
95
+ this.inflight.delete(installationId);
96
+ }
97
+ }
98
+ async acquireUnshared(installationId) {
99
+ const row = await this.store.getInstallation(installationId);
100
+ if (!row)
101
+ throw new UnknownInstallationError(installationId);
102
+ // Cache serves even while halted: a bad app key (or a kill switch)
103
+ // stops MINTING, but tokens minted before it stay usable on the
104
+ // merchant API until they expire.
105
+ if (row.token !== null && row.tokenExpiresAt !== null && this.isFresh(row.tokenExpiresAt)) {
106
+ return row.token;
107
+ }
108
+ if (this.halted)
109
+ throw this.halted;
110
+ const minted = await this.mintWithRetry(row.apiBase, installationId, row.installationPid);
111
+ // A successful mint proves the installation is active again: persist
112
+ // the fresh token and clear the pending mark in the same write.
113
+ await this.store.saveInstallation({
114
+ ...row,
115
+ token: minted.token,
116
+ tokenExpiresAt: minted.expiresAt,
117
+ tokenKid: minted.kid,
118
+ pending: false,
119
+ updatedAt: new Date(this.nowMs()).toISOString(),
120
+ });
121
+ return minted.token;
122
+ }
123
+ isFresh(expiresAtIso) {
124
+ const expiresMs = Date.parse(expiresAtIso);
125
+ if (Number.isNaN(expiresMs))
126
+ return false;
127
+ return expiresMs - this.nowMs() > TOKEN_VALIDITY_SKEW_SECONDS * 1000;
128
+ }
129
+ async dropCachedToken(installationId) {
130
+ await this.store.clearCachedToken(installationId);
131
+ }
132
+ /** Kill-switch path (also used by ops): forget every cached token, keep the rows. */
133
+ async dropAllCachedTokens() {
134
+ await this.store.clearAllCachedTokens();
135
+ }
136
+ /**
137
+ * Persist the 409 `app_installation_pending` mark for this installation
138
+ * (store column — survives restarts, B2 review r2). Resync's purge-absent
139
+ * step keeps marked rows (the list covers active installations only).
140
+ * Called by the mint path and by `resyncFromQueek`; idempotent, and a
141
+ * no-op when the row is already gone.
142
+ */
143
+ async markInstallationPending(installationId) {
144
+ await this.store.markInstallationPending(installationId);
145
+ }
146
+ /**
147
+ * Clear the persisted pending mark: the installation answered active
148
+ * again (mint success, resync 202/redelivery).
149
+ */
150
+ async clearInstallationPending(installationId) {
151
+ await this.store.clearInstallationPending(installationId);
152
+ }
153
+ /**
154
+ * True when the row carries the persisted pending mark. Consulted by
155
+ * resync's purge-absent step; false when the row is missing.
156
+ */
157
+ async isKnownPending(installationId) {
158
+ return this.store.isKnownPending(installationId);
159
+ }
160
+ /**
161
+ * Kill-switch path from any surface (mint 403, merchant 403, ops):
162
+ * forget EVERY cached token for the app and halt minting, loudly.
163
+ * Clears on process restart or `resumeMinting()`.
164
+ */
165
+ async revokeAppAccess() {
166
+ await this.store.clearAllCachedTokens();
167
+ this.halted = new AppMintHaltedError(APP_TOKEN_REVOKED_CODE, "Queek revoked this app's access (kill switch or disabled app). " +
168
+ "All cached tokens dropped. Re-enable the app, then restart (or resumeMinting).");
169
+ this.logger.error("queek app minting halted: access revoked (kill switch / disabled)", {
170
+ code: APP_TOKEN_REVOKED_CODE,
171
+ });
172
+ }
173
+ /** Sign a fresh app JWT (exposed for the app-API calls in `resync.ts`). */
174
+ signJwt(nowSeconds) {
175
+ return signAppJwt({ credential: this.credential, nowSeconds });
176
+ }
177
+ /**
178
+ * Map an app-endpoint failure to its contract behavior. ALWAYS throws:
179
+ * the halted error, or the original `QueekApiError` after performing the
180
+ * side effect (drop-all / purge). `installationPid` is log-only.
181
+ */
182
+ async handleAppEndpointError(error, installation) {
183
+ if (error.status === 401) {
184
+ // The wire contract defines 401 on app endpoints as `invalid_client`
185
+ // (bad/expired JWT, unknown kid): fatal for the app regardless of the
186
+ // exact code string — retry-looping a refused identity is never right.
187
+ this.halted = new AppMintHaltedError(INVALID_CLIENT_CODE, `Queek refused the app credential (status 401, code ${JSON.stringify(error.code)}). ` +
188
+ `Fix APP_PRIVATE_KEY/APP_KEY_ID or re-register, then restart (or resumeMinting).`);
189
+ this.logger.error("queek app minting halted: invalid_client", {
190
+ installation: installation.pid,
191
+ code: error.code,
192
+ });
193
+ throw this.halted;
194
+ }
195
+ if (error.status === 403 && isAppTokenRevoked(error.code)) {
196
+ await this.revokeAppAccess();
197
+ throw this.halted;
198
+ }
199
+ if (error.status === 404 && isInstallationGone(error.code)) {
200
+ // The row's deletion drops its pending mark with it — a gone
201
+ // installation is never "pending".
202
+ await this.store.deleteInstallation(installation.id);
203
+ this.logger.warn("queek installation gone server-side; purged locally", {
204
+ installation: installation.pid,
205
+ });
206
+ }
207
+ // NOTE: 409 `app_installation_pending` never reaches here as a purge
208
+ // or halt — the mint loop retries it with backoff below, and resync
209
+ // handles it per installation. A pending row is marked, never deleted.
210
+ throw error;
211
+ }
212
+ async mintWithRetry(apiBase, installationId, installationPid) {
213
+ const appBase = resolveAppApiBase(apiBase, this.allowedApiHosts);
214
+ let lastError;
215
+ for (let attempt = 0; attempt < MAX_MINT_ATTEMPTS; attempt++) {
216
+ try {
217
+ return await this.mintOnce(appBase, installationId, installationPid);
218
+ }
219
+ catch (error) {
220
+ lastError = error;
221
+ if (!(error instanceof QueekApiError))
222
+ throw error;
223
+ // Contract-mapped failures throw their final error from inside
224
+ // `handleAppEndpointError` (halt/drop/purge already performed).
225
+ if (error.status === 401 || error.status === 404 || error.status === 403) {
226
+ await this.handleAppEndpointError(error, { id: installationId, pid: installationPid });
227
+ }
228
+ if (error.status === 409 && isInstallationPending(error.code)) {
229
+ // Pending installation (rev 8, mark persisted per B2 review
230
+ // r2): retry later with backoff — NEVER purge, NEVER halt. The
231
+ // persisted mark keeps the row through restarts and resync's
232
+ // purge-absent step (the list covers active rows only). After
233
+ // the bounded budget the 409 propagates to the caller, which
234
+ // retries later; the mark clears on mint success (same write)
235
+ // or with the row on 404-gone purge.
236
+ await this.markInstallationPending(installationId);
237
+ this.logger.warn("queek installation pending; mint retry later with backoff", {
238
+ installation: installationPid,
239
+ code: error.code,
240
+ });
241
+ if (attempt === MAX_MINT_ATTEMPTS - 1)
242
+ throw error;
243
+ const backoff = Math.min(MINT_BACKOFF_MAX_MS, MINT_BACKOFF_BASE_MS * 2 ** attempt);
244
+ await this.sleep(this.jittered(backoff));
245
+ continue;
246
+ }
247
+ if (attempt === MAX_MINT_ATTEMPTS - 1)
248
+ throw error;
249
+ if (error.status === 429) {
250
+ await this.sleep(this.jittered(error.retryAfterMs ?? MINT_BACKOFF_BASE_MS));
251
+ continue;
252
+ }
253
+ if (error.status >= 500 || error.code === "network_error") {
254
+ const backoff = Math.min(MINT_BACKOFF_MAX_MS, MINT_BACKOFF_BASE_MS * 2 ** attempt);
255
+ await this.sleep(this.jittered(backoff));
256
+ continue;
257
+ }
258
+ throw error;
259
+ }
260
+ }
261
+ throw lastError;
262
+ }
263
+ jittered(baseMs) {
264
+ return baseMs + Math.floor(this.random() * RETRY_JITTER_MAX_MS);
265
+ }
266
+ async mintOnce(appBase, installationId, installationPid) {
267
+ // Signed per attempt (never reused past its ≤10-min window, never logged).
268
+ const jwt = signAppJwt({ credential: this.credential });
269
+ const url = `${appBase}${mintPath(installationId)}`;
270
+ const headers = new Headers();
271
+ headers.set("Authorization", `Bearer ${jwt}`);
272
+ headers.set("Accept", "application/json");
273
+ headers.set("User-Agent", this.userAgent);
274
+ let response;
275
+ try {
276
+ response = await this.fetchImpl(url, { method: "POST", headers });
277
+ }
278
+ catch (cause) {
279
+ throw new QueekApiError({
280
+ status: 0,
281
+ code: "network_error",
282
+ message: cause instanceof Error ? `Could not reach Queek: ${cause.message}` : "Could not reach Queek.",
283
+ });
284
+ }
285
+ const text = await response.text();
286
+ const parsed = text === "" ? null : tryParseJson(text);
287
+ if (!response.ok) {
288
+ throw queekApiErrorFromResponse(response.status, parsed, response.headers);
289
+ }
290
+ if (response.status !== 201) {
291
+ throw new Error(`Unexpected mint status ${response.status} for installation ${installationPid} (expected 201).`);
292
+ }
293
+ return parseMintBody(parsed, installationPid, this.credential.keyId);
294
+ }
295
+ }
296
+ function parseMintBody(body, installationPid, kid) {
297
+ const record = (typeof body === "object" && body !== null ? body : {});
298
+ const token = typeof record.token === "string" && record.token !== "" ? record.token : null;
299
+ const expiresAt = typeof record.expires_at === "string" ? record.expires_at : null;
300
+ if (token === null || expiresAt === null || Number.isNaN(Date.parse(expiresAt))) {
301
+ throw new Error(`Malformed mint response for installation ${installationPid}: expected {token, expires_at}.`);
302
+ }
303
+ return { token, expiresAt, kid };
304
+ }
305
+ /** Create a provider in one call (thin wrapper over `new AppTokenProvider`). */
306
+ export function createAppTokenProvider(options) {
307
+ return new AppTokenProvider(options);
308
+ }
309
+ /**
310
+ * A `QueekClient` whose every call goes through `acquireToken()`: the
311
+ * installation token is resolved per request (usually a cache hit — the
312
+ * store IS the cache, one shared row per installation), sent as
313
+ * `X-Client-Key`, and on a merchant-API token refusal (any 401, or 403
314
+ * `api_key_revoked` / `api_key_expired` / `invalid_client_key`) it is
315
+ * dropped, re-minted once, and the call retried once; a second refusal
316
+ * propagates to the caller. A merchant 403 `app_token_revoked` drops all
317
+ * cached tokens and halts minting; any other 403 propagates without a
318
+ * mint. 429/network retries (`requestWithRetry`) reuse one idempotency key
319
+ * exactly like the static client.
320
+ */
321
+ export function createInstallationClient(options) {
322
+ // Validated here, once, before any fetch — same rules as the static client.
323
+ const probe = resolveApiBase(options.apiBase, options.allowedApiHosts ?? []);
324
+ void probe;
325
+ const fetchImpl = options.fetchImpl ?? fetch;
326
+ const userAgent = options.userAgent ?? "queek-app/1.0";
327
+ const allowedApiHosts = options.allowedApiHosts ?? [];
328
+ let cached = null;
329
+ async function clientForToken(token) {
330
+ if (!cached || cached.token !== token) {
331
+ cached = {
332
+ token,
333
+ client: createQueekClient({
334
+ apiBase: options.apiBase,
335
+ apiKey: token,
336
+ fetchImpl,
337
+ userAgent,
338
+ allowedApiHosts,
339
+ }),
340
+ };
341
+ }
342
+ return cached.client;
343
+ }
344
+ async function request(method, path, requestOptions = {}) {
345
+ const token = await options.tokens.acquireToken(options.installationId);
346
+ try {
347
+ return await (await clientForToken(token)).request(method, path, requestOptions);
348
+ }
349
+ catch (error) {
350
+ if (!(error instanceof QueekApiError))
351
+ throw error;
352
+ // Kill switch / disabled app, observed on the merchant path: drop
353
+ // every cached token and halt minting, then propagate this refusal
354
+ // (no mint — the app is revoked, not the token).
355
+ if (error.status === 403 && isAppTokenRevoked(error.code)) {
356
+ await options.tokens.revokeAppAccess();
357
+ throw error;
358
+ }
359
+ // Token refusals ONLY (rev 7): any 401, or a 403 carrying a
360
+ // revoked/expired-key code. Every other 403 (scope, plan, mode)
361
+ // propagates to the caller without burning a mint.
362
+ if (!isTokenRefusal(error.status, error.code))
363
+ throw error;
364
+ // Dead token: drop it, re-mint once, retry once. The re-mint runs
365
+ // the full contract error mapping (halt/purge on
366
+ // invalid_client/kill-switch/gone); a second merchant refusal
367
+ // propagates to the caller.
368
+ await options.tokens.dropCachedToken(options.installationId);
369
+ const fresh = await options.tokens.acquireToken(options.installationId);
370
+ return (await clientForToken(fresh)).request(method, path, requestOptions);
371
+ }
372
+ }
373
+ async function requestWithRetry(method, path, retryOptions = {}) {
374
+ const maxAttempts = Math.max(1, Math.floor(retryOptions.maxAttempts ?? 3));
375
+ const baseDelayMs = retryOptions.baseDelayMs ?? 250;
376
+ const maxDelayMs = retryOptions.maxDelayMs ?? 5000;
377
+ const sleep = retryOptions.sleep ?? defaultSleep;
378
+ const idempotencyKey = retryOptions.idempotencyKey;
379
+ let lastError;
380
+ for (let attempt = 0; attempt < maxAttempts; attempt++) {
381
+ try {
382
+ return await request(method, path, { ...retryOptions, idempotencyKey });
383
+ }
384
+ catch (error) {
385
+ lastError = error;
386
+ const retryable = error instanceof QueekApiError && (error.isRateLimited || error.code === "network_error");
387
+ if (!retryable || attempt === maxAttempts - 1)
388
+ throw error;
389
+ const delay = error instanceof QueekApiError && error.isRateLimited && error.retryAfterMs !== undefined
390
+ ? error.retryAfterMs
391
+ : Math.min(maxDelayMs, baseDelayMs * 2 ** attempt);
392
+ await sleep(delay);
393
+ }
394
+ }
395
+ throw lastError;
396
+ }
397
+ return {
398
+ getStore: (signal) => request("GET", "/store", { signal }),
399
+ request,
400
+ requestWithRetry,
401
+ };
402
+ }
403
+ function tryParseJson(text) {
404
+ try {
405
+ return JSON.parse(text);
406
+ }
407
+ catch {
408
+ return { message: text.slice(0, 500) };
409
+ }
410
+ }
411
+ //# sourceMappingURL=tokens.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tokens.js","sourceRoot":"","sources":["../src/tokens.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,60 @@
1
+ import { Hono } from "hono";
2
+ import type { InstallationRecord, InstallationStore } from "./store.js";
3
+ /**
4
+ * Receiver for Queek topic deliveries (`DeliverWebhookJob` in
5
+ * queek_backend, one signed POST per installation endpoint).
6
+ *
7
+ * Delivery envelope: `{ id, topic, api_version: "v1", created_at, data }`
8
+ * with `X-Queek-Topic` echoing `topic`. Verification uses the
9
+ * PER-INSTALLATION endpoint secret handed over once inside the install
10
+ * payload (`data.webhook_secret`) — never the app signing secret.
11
+ *
12
+ * Secret lookup: topic payloads are resource renders with no guaranteed
13
+ * installation/vendor id, so the default resolver identifies the
14
+ * installation by trying each stored installation secret against the
15
+ * signature (the signature itself names the sender; HMACs are microseconds,
16
+ * installations per app are few). Apps that carry their own routing hint
17
+ * may pass `resolveSecret` instead.
18
+ *
19
+ * Dedupe: Queek retries a delivery for ~4h until it sees 2xx, so every
20
+ * `webhook-id` is processed AT MOST once — the header id is claimed
21
+ * atomically up front (exactly one concurrent same-id delivery runs the
22
+ * handler) and a repeat answers 200 WITHOUT re-running it. A throwing
23
+ * handler releases the claim and answers 500 so the retry can land.
24
+ * Unknown topics answer 200 too (a topic the app no longer handles must
25
+ * not wedge Queek's retry queue); only auth and malformed bodies are
26
+ * non-2xx.
27
+ */
28
+ export declare const QUEEK_TOPIC_HEADER = "X-Queek-Topic";
29
+ export interface QueekWebhookEnvelope<TData = unknown> {
30
+ id: string;
31
+ topic: string;
32
+ api_version: "v1";
33
+ created_at: string;
34
+ data: TData;
35
+ }
36
+ export interface WebhookHandlerContext {
37
+ installation: InstallationRecord;
38
+ topic: string;
39
+ eventId: string;
40
+ }
41
+ export type WebhookHandlerFn<TData = unknown> = (envelope: QueekWebhookEnvelope<TData>, context: WebhookHandlerContext) => Promise<void>;
42
+ export interface SecretResolution {
43
+ installationId: string;
44
+ secret: string;
45
+ }
46
+ export interface WebhookHandlerOptions {
47
+ store: InstallationStore;
48
+ /** `topic → handler`, e.g. `{ "orders/updated": onOrderUpdated }`. */
49
+ handlers: Record<string, WebhookHandlerFn>;
50
+ /** Override the default try-each-secret resolution. */
51
+ resolveSecret?: (envelope: QueekWebhookEnvelope | null, rawBody: string, headers: {
52
+ id: string;
53
+ timestamp: string;
54
+ signatureHeader: string;
55
+ }) => Promise<SecretResolution | null>;
56
+ nowSeconds?: number;
57
+ maxSkewSeconds?: number;
58
+ }
59
+ export declare function createWebhookHandler(options: WebhookHandlerOptions): Hono;
60
+ //# sourceMappingURL=webhooks.d.ts.map