@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
package/dist/resync.js ADDED
@@ -0,0 +1,203 @@
1
+ import { isInstallationGone, isInstallationPending, isResyncCooldown, RETRY_JITTER_MAX_MS, } from "./app-auth.js";
2
+ import { QueekApiError, queekApiErrorFromResponse } from "./client.js";
3
+ import { createLogger } from "./logger.js";
4
+ import { resolveAppApiBase } from "./tokens.js";
5
+ const MAX_LIST_ATTEMPTS = 3;
6
+ const LIST_BACKOFF_BASE_MS = 250;
7
+ const LIST_BACKOFF_MAX_MS = 5_000;
8
+ /** Per-installation resync attempts before a retryable refusal is skipped or aborts the run. */
9
+ const MAX_RESYNC_ATTEMPTS = 3;
10
+ const RESYNC_BACKOFF_BASE_MS = 250;
11
+ const RESYNC_BACKOFF_MAX_MS = 5_000;
12
+ function defaultSleep(ms) {
13
+ return new Promise((resolve) => setTimeout(resolve, ms));
14
+ }
15
+ export async function resyncFromQueek(options) {
16
+ const fetchImpl = options.fetchImpl ?? fetch;
17
+ const userAgent = options.userAgent ?? "queek-app/1.0";
18
+ const logger = options.logger ?? createLogger({ service: "queek-app-resync" });
19
+ const sleep = options.sleep ?? defaultSleep;
20
+ const random = options.random ?? Math.random;
21
+ const jittered = (baseMs) => baseMs + Math.floor(random() * RETRY_JITTER_MAX_MS);
22
+ const appBase = resolveAppApiBase(options.apiBase, options.allowedApiHosts ?? []);
23
+ async function appFetch(path, init) {
24
+ // Signed per attempt (never reused past its ≤10-min window, never logged).
25
+ const jwt = options.tokens.signJwt();
26
+ const headers = new Headers();
27
+ headers.set("Authorization", `Bearer ${jwt}`);
28
+ headers.set("Accept", "application/json");
29
+ headers.set("User-Agent", userAgent);
30
+ let response;
31
+ try {
32
+ response = await fetchImpl(`${appBase}${path}`, { method: init.method, headers });
33
+ }
34
+ catch (cause) {
35
+ throw new QueekApiError({
36
+ status: 0,
37
+ code: "network_error",
38
+ message: cause instanceof Error ? `Could not reach Queek: ${cause.message}` : "Could not reach Queek.",
39
+ });
40
+ }
41
+ return response;
42
+ }
43
+ /** Parse an app-endpoint failure without acting on it: the caller below
44
+ * decides by status + code (pending/cooldown/throttle/gone/halt). */
45
+ async function readAppError(response) {
46
+ const text = await response.text();
47
+ const parsed = text === "" ? null : tryParseJson(text);
48
+ return queekApiErrorFromResponse(response.status, parsed, response.headers);
49
+ }
50
+ // -- List (opaque keyset cursor, secret-free) ------------------------------
51
+ // The cursor is never interpreted — echoed back verbatim until
52
+ // `next_cursor` is null (rev 8: keyset on a stable unique key, scoped to
53
+ // the calling app; inserts/uninstalls mid-walk never skip or duplicate).
54
+ const listed = [];
55
+ let cursor = null;
56
+ let attempt = 0;
57
+ let listDone = false;
58
+ while (!listDone) {
59
+ const path = cursor === null ? `/installations` : `/installations?cursor=${encodeURIComponent(cursor)}`;
60
+ let response;
61
+ try {
62
+ response = await appFetch(path, { method: "GET" });
63
+ }
64
+ catch (error) {
65
+ if (error instanceof QueekApiError &&
66
+ error.code === "network_error" &&
67
+ attempt < MAX_LIST_ATTEMPTS - 1) {
68
+ await sleep(jittered(Math.min(LIST_BACKOFF_MAX_MS, LIST_BACKOFF_BASE_MS * 2 ** attempt)));
69
+ attempt += 1;
70
+ continue;
71
+ }
72
+ throw error;
73
+ }
74
+ if (response.status === 429) {
75
+ if (attempt < MAX_LIST_ATTEMPTS - 1) {
76
+ const retryAfter = queekApiErrorFromResponse(429, null, response.headers).retryAfterMs;
77
+ await sleep(jittered(retryAfter ?? LIST_BACKOFF_BASE_MS));
78
+ attempt += 1;
79
+ continue;
80
+ }
81
+ throw queekApiErrorFromResponse(response.status, null, response.headers);
82
+ }
83
+ if (!response.ok || response.status !== 200) {
84
+ // 401/403 halt inside (invalid_client / kill switch); 404-gone is a
85
+ // no-op against the empty id; anything else propagates.
86
+ await options.tokens.handleAppEndpointError(await readAppError(response), { id: "", pid: "" });
87
+ }
88
+ const page = (await response.json());
89
+ for (const item of page.data ?? [])
90
+ listed.push(item);
91
+ attempt = 0; // a good page resets the backoff budget for the next page
92
+ cursor = typeof page.next_cursor === "string" && page.next_cursor !== "" ? page.next_cursor : null;
93
+ listDone = cursor === null;
94
+ }
95
+ // -- Resync each, drop cached tokens, purge the absent ----------------------
96
+ const result = {
97
+ listed: [],
98
+ resyncRequested: [],
99
+ cooldownSkipped: [],
100
+ pendingSkipped: [],
101
+ purged: [],
102
+ };
103
+ const listedIds = new Set();
104
+ for (const item of listed) {
105
+ listedIds.add(item.id);
106
+ result.listed.push(item.id);
107
+ const resyncPath = `/installations/${encodeURIComponent(item.id)}/resync`;
108
+ for (let attempt = 0;; attempt++) {
109
+ const response = await appFetch(resyncPath, { method: "POST" });
110
+ if (response.status === 202) {
111
+ result.resyncRequested.push(item.id);
112
+ await options.tokens.clearInstallationPending(item.id);
113
+ break;
114
+ }
115
+ const error = await readAppError(response);
116
+ if (response.status === 429 && isResyncCooldown(error.code)) {
117
+ // Rotation cooldown (≤1/hour): skip this installation, no retry loop.
118
+ result.cooldownSkipped.push(item.id);
119
+ logger.warn("queek resync skipped: rotation cooldown", { installation: item.id });
120
+ break;
121
+ }
122
+ if (response.status === 429) {
123
+ // Per-app bucket (rev 8): back off on `Retry-After` (jittered) and
124
+ // retry — never recorded as a cooldown skip. An exhausted budget
125
+ // aborts the run: the whole app is throttled, so pushing on would
126
+ // only deepen it; the caller retries later.
127
+ if (attempt >= MAX_RESYNC_ATTEMPTS - 1)
128
+ throw error;
129
+ await sleep(jittered(error.retryAfterMs ?? RESYNC_BACKOFF_BASE_MS));
130
+ continue;
131
+ }
132
+ if (response.status === 409 && isInstallationPending(error.code)) {
133
+ // Pending installation (rev 8, mark persisted per B2 review r2):
134
+ // retry later with backoff — NEVER purge. An exhausted budget
135
+ // skips (recorded); the persisted mark survives restarts, so the
136
+ // purge-absent step below (and after a restart) keeps the row.
137
+ await options.tokens.markInstallationPending(item.id);
138
+ if (attempt >= MAX_RESYNC_ATTEMPTS - 1) {
139
+ result.pendingSkipped.push(item.id);
140
+ logger.warn("queek resync skipped: installation pending", { installation: item.id });
141
+ break;
142
+ }
143
+ const backoff = Math.min(RESYNC_BACKOFF_MAX_MS, RESYNC_BACKOFF_BASE_MS * 2 ** attempt);
144
+ await sleep(jittered(backoff));
145
+ continue;
146
+ }
147
+ try {
148
+ // 404-gone purges inside; 401/403-halt throws the halted error; a
149
+ // 409 with any OTHER code rethrows here untouched (never purged).
150
+ await options.tokens.handleAppEndpointError(error, { id: item.id, pid: item.id });
151
+ }
152
+ catch (endpointError) {
153
+ // Gone server-side: the row is already purged inside — record it
154
+ // and keep going. Anything else (halt et al.) aborts the run.
155
+ if (endpointError instanceof QueekApiError &&
156
+ endpointError.status === 404 &&
157
+ isInstallationGone(endpointError.code)) {
158
+ result.purged.push(item.id);
159
+ break;
160
+ }
161
+ throw endpointError;
162
+ }
163
+ }
164
+ }
165
+ // Drop cached tokens for everything still listed (a post-outage token
166
+ // may be revoked server-side; the next call re-mints cleanly)…
167
+ for (const id of listedIds) {
168
+ await options.store.clearCachedToken(id);
169
+ }
170
+ // …and purge local rows Queek no longer lists (uninstall-while-down) —
171
+ // EXCEPT rows carrying the persisted pending mark. The list covers
172
+ // ACTIVE installations only (rev 8), so absence alone never purges a
173
+ // pending row — including after a process restart, because the mark
174
+ // lives in the store column, not in memory. The row stays for a later
175
+ // run, when it is active (listed + resynced), gone (404 → purged), or
176
+ // still pending (kept again).
177
+ const local = await options.store.listInstallations();
178
+ for (const row of local) {
179
+ if (!listedIds.has(row.installationId)) {
180
+ if (await options.tokens.isKnownPending(row.installationId)) {
181
+ logger.warn("queek resync kept a pending installation absent from Queek's list", {
182
+ installation: row.installationPid,
183
+ });
184
+ continue;
185
+ }
186
+ await options.store.deleteInstallation(row.installationId);
187
+ result.purged.push(row.installationId);
188
+ logger.warn("queek resync purged an installation absent from Queek's list", {
189
+ installation: row.installationPid,
190
+ });
191
+ }
192
+ }
193
+ return result;
194
+ }
195
+ function tryParseJson(text) {
196
+ try {
197
+ return JSON.parse(text);
198
+ }
199
+ catch {
200
+ return { message: text.slice(0, 500) };
201
+ }
202
+ }
203
+ //# sourceMappingURL=resync.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resync.js","sourceRoot":"","sources":["../src/resync.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Server-only entry (S4 stage 2): session-token verification needs the
3
+ * per-installation `embsec_…` secret, which must never enter a browser
4
+ * bundle. Import the verifier from `@usequeek/app-sdk/server`, never from
5
+ * the main entry.
6
+ */
7
+ export type { SessionTokenBinding, SessionTokenClaims, SessionTokenFailure, VerifySessionTokenOptions, } from "./session.js";
8
+ export { EMBED_SECRET_PREFIX, SESSION_CLOCK_TOLERANCE_SECONDS, sessionTokenInstallationId, verifySessionToken, verifySessionTokenDetailed, } from "./session.js";
9
+ //# sourceMappingURL=server.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,YAAY,EACV,mBAAmB,EACnB,kBAAkB,EAClB,mBAAmB,EACnB,yBAAyB,GAC1B,MAAM,cAAc,CAAC;AACtB,OAAO,EACL,mBAAmB,EACnB,+BAA+B,EAC/B,0BAA0B,EAC1B,kBAAkB,EAClB,0BAA0B,GAC3B,MAAM,cAAc,CAAC"}
package/dist/server.js ADDED
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Server-only entry (S4 stage 2): session-token verification needs the
3
+ * per-installation `embsec_…` secret, which must never enter a browser
4
+ * bundle. Import the verifier from `@usequeek/app-sdk/server`, never from
5
+ * the main entry.
6
+ */
7
+ export { EMBED_SECRET_PREFIX, SESSION_CLOCK_TOLERANCE_SECONDS, sessionTokenInstallationId, verifySessionToken, verifySessionTokenDetailed, } from "./session.js";
8
+ //# sourceMappingURL=server.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"server.js","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAQH,OAAO,EACL,mBAAmB,EACnB,+BAA+B,EAC/B,0BAA0B,EAC1B,kBAAkB,EAClB,0BAA0B,GAC3B,MAAM,cAAc,CAAC"}
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Dashboard session tokens (S4 stage 2): short-lived HS256 JWTs the backend
3
+ * mints per installation for the framed merchant page. The app backend
4
+ * verifies them with the per-installation `embsec_…` secret before trusting
5
+ * any call that carries one.
6
+ *
7
+ * Server-only: the secret must never enter a browser bundle, so this module
8
+ * ships behind the `./server` export, not the main entry. Style mirrors
9
+ * `signatures.ts` — a boolean fast path plus a detailed variant returning
10
+ * `{ ok } | { ok: false, reason }`.
11
+ */
12
+ export declare const EMBED_SECRET_PREFIX = "embsec_";
13
+ /** The only JWS algorithm the dashboard mints (HS256 over the embsec_ secret). */
14
+ export declare const SESSION_TOKEN_ALG = "HS256";
15
+ /** Backend skew the verifier tolerates (matches the mint side). */
16
+ export declare const SESSION_CLOCK_TOLERANCE_SECONDS = 20;
17
+ export type SessionTokenFailure = "missing_token" | "missing_secret" | "wrong_algorithm" | "invalid_signature" | "expired" | "not_yet_valid" | "wrong_audience" | "wrong_issuer" | "binding_mismatch";
18
+ export interface SessionTokenClaims {
19
+ installationId: string;
20
+ vendorId: string;
21
+ appSlug: string;
22
+ appId: string;
23
+ subject: string;
24
+ sessionId: string;
25
+ issuedAt: number;
26
+ expiresAt: number;
27
+ }
28
+ export interface SessionTokenBinding {
29
+ installationId: string;
30
+ vendorId: string;
31
+ appSlug: string;
32
+ appId: string;
33
+ }
34
+ export interface VerifySessionTokenOptions {
35
+ /** The per-installation `embsec_…` secret, raw UTF-8 bytes (never decoded). */
36
+ secret: string;
37
+ /** Single expected audience: the app slug. */
38
+ audience: string;
39
+ /** Expected issuer: the Queek api_base. */
40
+ issuer: string;
41
+ /**
42
+ * The installation row the token must belong to (the app backend's own
43
+ * record). Every field is compared — a token minted for another
44
+ * installation fails closed even when its signature and audience check
45
+ * out, mirroring the backend `verify()`.
46
+ */
47
+ expected: SessionTokenBinding;
48
+ clockToleranceSeconds?: number;
49
+ }
50
+ export declare function verifySessionToken(token: string, options: VerifySessionTokenOptions): Promise<boolean>;
51
+ export declare function verifySessionTokenDetailed(token: string, options: VerifySessionTokenOptions): Promise<{
52
+ ok: true;
53
+ claims: SessionTokenClaims;
54
+ } | {
55
+ ok: false;
56
+ reason: SessionTokenFailure;
57
+ }>;
58
+ /**
59
+ * ROUTING HINT ONLY — the `installation_id` claim read WITHOUT verifying
60
+ * anything, so a server can load the installation whose embed secret then
61
+ * verifies the token with `verifySessionToken`. Never trust it on its
62
+ * own: a caller that skips the verify has no authentication at all.
63
+ * Null for anything that is not a decodable JWT carrying the claim.
64
+ */
65
+ export declare function sessionTokenInstallationId(token: string): string | null;
66
+ //# sourceMappingURL=session.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session.d.ts","sourceRoot":"","sources":["../src/session.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;GAUG;AAEH,eAAO,MAAM,mBAAmB,YAAY,CAAC;AAE7C,kFAAkF;AAClF,eAAO,MAAM,iBAAiB,UAAU,CAAC;AAEzC,mEAAmE;AACnE,eAAO,MAAM,+BAA+B,KAAK,CAAC;AAElD,MAAM,MAAM,mBAAmB,GAC3B,eAAe,GACf,gBAAgB,GAChB,iBAAiB,GACjB,mBAAmB,GACnB,SAAS,GACT,eAAe,GACf,gBAAgB,GAChB,cAAc,GACd,kBAAkB,CAAC;AAEvB,MAAM,WAAW,kBAAkB;IACjC,cAAc,EAAE,MAAM,CAAC;IACvB,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,mBAAmB;IAClC,cAAc,EAAE,MAAM,CAAC;IACvB,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,yBAAyB;IACxC,+EAA+E;IAC/E,MAAM,EAAE,MAAM,CAAC;IACf,8CAA8C;IAC9C,QAAQ,EAAE,MAAM,CAAC;IACjB,2CAA2C;IAC3C,MAAM,EAAE,MAAM,CAAC;IACf;;;;;OAKG;IACH,QAAQ,EAAE,mBAAmB,CAAC;IAC9B,qBAAqB,CAAC,EAAE,MAAM,CAAC;CAChC;AAED,wBAAsB,kBAAkB,CACtC,KAAK,EAAE,MAAM,EACb,OAAO,EAAE,yBAAyB,GACjC,OAAO,CAAC,OAAO,CAAC,CAGlB;AAED,wBAAsB,0BAA0B,CAC9C,KAAK,EAAE,MAAM,EACb,OAAO,EAAE,yBAAyB,GACjC,OAAO,CAAC;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,MAAM,EAAE,kBAAkB,CAAA;CAAE,GAAG;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,mBAAmB,CAAA;CAAE,CAAC,CA2ChG;AAED;;;;;;GAMG;AACH,wBAAgB,0BAA0B,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAOvE"}
@@ -0,0 +1,155 @@
1
+ import { decodeJwt, decodeProtectedHeader, jwtVerify } from "jose";
2
+ /**
3
+ * Dashboard session tokens (S4 stage 2): short-lived HS256 JWTs the backend
4
+ * mints per installation for the framed merchant page. The app backend
5
+ * verifies them with the per-installation `embsec_…` secret before trusting
6
+ * any call that carries one.
7
+ *
8
+ * Server-only: the secret must never enter a browser bundle, so this module
9
+ * ships behind the `./server` export, not the main entry. Style mirrors
10
+ * `signatures.ts` — a boolean fast path plus a detailed variant returning
11
+ * `{ ok } | { ok: false, reason }`.
12
+ */
13
+ export const EMBED_SECRET_PREFIX = "embsec_";
14
+ /** The only JWS algorithm the dashboard mints (HS256 over the embsec_ secret). */
15
+ export const SESSION_TOKEN_ALG = "HS256";
16
+ /** Backend skew the verifier tolerates (matches the mint side). */
17
+ export const SESSION_CLOCK_TOLERANCE_SECONDS = 20;
18
+ export async function verifySessionToken(token, options) {
19
+ const result = await verifySessionTokenDetailed(token, options);
20
+ return result.ok;
21
+ }
22
+ export async function verifySessionTokenDetailed(token, options) {
23
+ if (!token) {
24
+ return { ok: false, reason: "missing_token" };
25
+ }
26
+ const { secret, audience, issuer } = options;
27
+ if (!secret?.startsWith(EMBED_SECRET_PREFIX)) {
28
+ return { ok: false, reason: "missing_secret" };
29
+ }
30
+ // Fail closed on the algorithm before jose sees the token: a foreign alg
31
+ // (none, RS256, …) is rejected here with wrong_algorithm regardless of
32
+ // how jose shapes the downstream error (key-type TypeError vs
33
+ // JOSEAlgNotAllowed). An undecodable header falls through to jwtVerify,
34
+ // which rejects it as invalid_signature.
35
+ const headerAlg = readTokenAlg(token);
36
+ if (headerAlg !== null && headerAlg !== SESSION_TOKEN_ALG) {
37
+ return { ok: false, reason: "wrong_algorithm" };
38
+ }
39
+ let payload;
40
+ try {
41
+ const verified = await jwtVerify(token, new TextEncoder().encode(secret), {
42
+ algorithms: ["HS256"],
43
+ audience,
44
+ issuer,
45
+ clockTolerance: options.clockToleranceSeconds ?? SESSION_CLOCK_TOLERANCE_SECONDS,
46
+ });
47
+ payload = verified.payload;
48
+ }
49
+ catch (error) {
50
+ return { ok: false, reason: classifyJoseError(error) };
51
+ }
52
+ const claims = readSessionClaims(payload);
53
+ const { expected } = options;
54
+ if (!claims ||
55
+ claims.installationId !== expected.installationId ||
56
+ claims.vendorId !== expected.vendorId ||
57
+ claims.appSlug !== expected.appSlug ||
58
+ claims.appId !== expected.appId) {
59
+ return { ok: false, reason: "binding_mismatch" };
60
+ }
61
+ return { ok: true, claims };
62
+ }
63
+ /**
64
+ * ROUTING HINT ONLY — the `installation_id` claim read WITHOUT verifying
65
+ * anything, so a server can load the installation whose embed secret then
66
+ * verifies the token with `verifySessionToken`. Never trust it on its
67
+ * own: a caller that skips the verify has no authentication at all.
68
+ * Null for anything that is not a decodable JWT carrying the claim.
69
+ */
70
+ export function sessionTokenInstallationId(token) {
71
+ try {
72
+ const claim = decodeJwt(token).installation_id;
73
+ return typeof claim === "string" && claim.length > 0 ? claim : null;
74
+ }
75
+ catch {
76
+ return null;
77
+ }
78
+ }
79
+ /**
80
+ * The token's `alg` without verifying anything. Returns null when the
81
+ * header is missing or undecodable, so malformed tokens keep flowing to
82
+ * jwtVerify (invalid_signature) instead of being mislabelled.
83
+ */
84
+ function readTokenAlg(token) {
85
+ try {
86
+ const alg = decodeProtectedHeader(token).alg;
87
+ return typeof alg === "string" ? alg : null;
88
+ }
89
+ catch {
90
+ return null;
91
+ }
92
+ }
93
+ function readSessionClaims(payload) {
94
+ const pick = (key) => typeof payload[key] === "string" && payload[key].length > 0 ? payload[key] : null;
95
+ const installationId = pick("installation_id");
96
+ const vendorId = pick("vendor_id");
97
+ const appSlug = pick("app_slug");
98
+ const appId = pick("app_id");
99
+ const subject = typeof payload.sub === "string" ? payload.sub : null;
100
+ const sessionId = pick("sid");
101
+ const issuedAt = typeof payload.iat === "number" ? payload.iat : null;
102
+ const expiresAt = typeof payload.exp === "number" ? payload.exp : null;
103
+ if (!installationId ||
104
+ !vendorId ||
105
+ !appSlug ||
106
+ !appId ||
107
+ !subject ||
108
+ !sessionId ||
109
+ issuedAt === null ||
110
+ expiresAt === null) {
111
+ return null;
112
+ }
113
+ return { installationId, vendorId, appSlug, appId, subject, sessionId, issuedAt, expiresAt };
114
+ }
115
+ function classifyJoseError(error) {
116
+ // Exact jose v6 pins (verified against jose@6.2.12 — codes, names, and
117
+ // the `claim` field probed, not guessed): claim failures carry code
118
+ // ERR_JWT_CLAIM_VALIDATION_FAILED and name the claim; expiry also
119
+ // surfaces as JWTExpired / ERR_JWT_EXPIRED; algorithm refusal is
120
+ // JOSEAlgNotAllowed / ERR_JOSE_ALG_NOT_ALLOWED (unreachable in practice —
121
+ // the header pre-check fails closed first). Anything else — JWSInvalid,
122
+ // JWSSignatureVerificationFailed, key TypeErrors, garbage input — is an
123
+ // invalid signature. No fuzzy matching: a renamed jose error must fall
124
+ // through loudly, not hide behind a regex.
125
+ const shaped = error;
126
+ const claim = typeof shaped?.claim === "string" ? shaped.claim : "";
127
+ if (claim === "exp") {
128
+ return "expired";
129
+ }
130
+ if (claim === "nbf") {
131
+ return "not_yet_valid";
132
+ }
133
+ if (claim === "aud") {
134
+ return "wrong_audience";
135
+ }
136
+ if (claim === "iss") {
137
+ return "wrong_issuer";
138
+ }
139
+ const code = typeof shaped?.code === "string" ? shaped.code : "";
140
+ if (code === "ERR_JWT_EXPIRED") {
141
+ return "expired";
142
+ }
143
+ if (code === "ERR_JOSE_ALG_NOT_ALLOWED") {
144
+ return "wrong_algorithm";
145
+ }
146
+ const name = typeof shaped?.name === "string" ? shaped.name : "";
147
+ if (name === "JWTExpired") {
148
+ return "expired";
149
+ }
150
+ if (name === "JOSEAlgNotAllowed") {
151
+ return "wrong_algorithm";
152
+ }
153
+ return "invalid_signature";
154
+ }
155
+ //# sourceMappingURL=session.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session.js","sourceRoot":"","sources":["../src/session.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Standard Webhooks verification, byte-for-byte compatible with Queek's
3
+ * `App\Services\Webhooks\WebhookSigner` (queek_backend).
4
+ *
5
+ * Signed content: `{id}.{timestamp}.{body}` where `body` is the RAW request
6
+ * bytes. Re-serialising JSON before verifying breaks the MAC — verify first,
7
+ * parse second.
8
+ *
9
+ * The MAC key is the DECODED bytes of the `whsec_<base64>` secret, not the
10
+ * printable string. A secret without the prefix (or with undecodable base64)
11
+ * falls back to the raw string, exactly like the backend's `keyFor()`.
12
+ *
13
+ * Header names: `webhook-id`, `webhook-timestamp`, `webhook-signature`.
14
+ * The signature header may carry several space-delimited `v1,<base64>`
15
+ * signatures (rotation grace window): ANY match verifies.
16
+ */
17
+ export declare const WEBHOOK_ID_HEADER = "webhook-id";
18
+ export declare const WEBHOOK_TIMESTAMP_HEADER = "webhook-timestamp";
19
+ export declare const WEBHOOK_SIGNATURE_HEADER = "webhook-signature";
20
+ export declare const SECRET_PREFIX = "whsec_";
21
+ /** Maximum age of a delivery before it is rejected as stale (5 minutes, the Standard Webhooks default). */
22
+ export declare const MAX_TIMESTAMP_SKEW_SECONDS: number;
23
+ export interface SignatureInput {
24
+ /** Value of the `webhook-id` header. */
25
+ id: string;
26
+ /** Value of the `webhook-timestamp` header (unix seconds, as sent). */
27
+ timestamp: string;
28
+ /** Raw request body bytes, untouched. */
29
+ body: string;
30
+ /** Value of the `webhook-signature` header (one or more space-delimited signatures). */
31
+ signatureHeader: string;
32
+ /** The endpoint/app `whsec_…` secret. */
33
+ secret: string;
34
+ }
35
+ export interface VerifyOptions {
36
+ /** Unix seconds to judge freshness against (defaults to now). Exposed for tests. */
37
+ nowSeconds?: number;
38
+ /** Allowed clock skew in seconds (defaults to MAX_TIMESTAMP_SKEW_SECONDS). */
39
+ maxSkewSeconds?: number;
40
+ /** Skip the freshness check (NOT recommended; exposed for replaying stored fixtures). */
41
+ skipFreshnessCheck?: boolean;
42
+ }
43
+ export declare function secretKeyBytes(secret: string): Buffer;
44
+ /** One `v1,<base64>` signature for one secret — mirrors `WebhookSigner::sign()`. */
45
+ export declare function signQueekPayload(eventId: string, timestamp: number | string, body: string, secret: string): string;
46
+ /** Failure reason when verification rejects a delivery. */
47
+ export type SignatureFailure = "missing_headers" | "stale_timestamp" | "signature_mismatch";
48
+ export declare function verifyQueekSignature(input: SignatureInput, options?: VerifyOptions): boolean;
49
+ export declare function verifyQueekSignatureDetailed(input: SignatureInput, options?: VerifyOptions): {
50
+ ok: true;
51
+ } | {
52
+ ok: false;
53
+ reason: SignatureFailure;
54
+ };
55
+ //# sourceMappingURL=signatures.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"signatures.d.ts","sourceRoot":"","sources":["../src/signatures.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;GAeG;AAEH,eAAO,MAAM,iBAAiB,eAAe,CAAC;AAC9C,eAAO,MAAM,wBAAwB,sBAAsB,CAAC;AAC5D,eAAO,MAAM,wBAAwB,sBAAsB,CAAC;AAC5D,eAAO,MAAM,aAAa,WAAW,CAAC;AAEtC,2GAA2G;AAC3G,eAAO,MAAM,0BAA0B,QAAS,CAAC;AAEjD,MAAM,WAAW,cAAc;IAC7B,wCAAwC;IACxC,EAAE,EAAE,MAAM,CAAC;IACX,uEAAuE;IACvE,SAAS,EAAE,MAAM,CAAC;IAClB,yCAAyC;IACzC,IAAI,EAAE,MAAM,CAAC;IACb,wFAAwF;IACxF,eAAe,EAAE,MAAM,CAAC;IACxB,yCAAyC;IACzC,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,aAAa;IAC5B,oFAAoF;IACpF,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,8EAA8E;IAC9E,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,yFAAyF;IACzF,kBAAkB,CAAC,EAAE,OAAO,CAAC;CAC9B;AAED,wBAAgB,cAAc,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAarD;AAED,oFAAoF;AACpF,wBAAgB,gBAAgB,CAC9B,OAAO,EAAE,MAAM,EACf,SAAS,EAAE,MAAM,GAAG,MAAM,EAC1B,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,MAAM,GACb,MAAM,CAKR;AAED,2DAA2D;AAC3D,MAAM,MAAM,gBAAgB,GAAG,iBAAiB,GAAG,iBAAiB,GAAG,oBAAoB,CAAC;AAE5F,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,cAAc,EAAE,OAAO,GAAE,aAAkB,GAAG,OAAO,CAEhG;AAED,wBAAgB,4BAA4B,CAC1C,KAAK,EAAE,cAAc,EACrB,OAAO,GAAE,aAAkB,GAC1B;IAAE,EAAE,EAAE,IAAI,CAAA;CAAE,GAAG;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,gBAAgB,CAAA;CAAE,CAyBxD"}
@@ -0,0 +1,74 @@
1
+ import { createHmac, timingSafeEqual } from "node:crypto";
2
+ /**
3
+ * Standard Webhooks verification, byte-for-byte compatible with Queek's
4
+ * `App\Services\Webhooks\WebhookSigner` (queek_backend).
5
+ *
6
+ * Signed content: `{id}.{timestamp}.{body}` where `body` is the RAW request
7
+ * bytes. Re-serialising JSON before verifying breaks the MAC — verify first,
8
+ * parse second.
9
+ *
10
+ * The MAC key is the DECODED bytes of the `whsec_<base64>` secret, not the
11
+ * printable string. A secret without the prefix (or with undecodable base64)
12
+ * falls back to the raw string, exactly like the backend's `keyFor()`.
13
+ *
14
+ * Header names: `webhook-id`, `webhook-timestamp`, `webhook-signature`.
15
+ * The signature header may carry several space-delimited `v1,<base64>`
16
+ * signatures (rotation grace window): ANY match verifies.
17
+ */
18
+ export const WEBHOOK_ID_HEADER = "webhook-id";
19
+ export const WEBHOOK_TIMESTAMP_HEADER = "webhook-timestamp";
20
+ export const WEBHOOK_SIGNATURE_HEADER = "webhook-signature";
21
+ export const SECRET_PREFIX = "whsec_";
22
+ /** Maximum age of a delivery before it is rejected as stale (5 minutes, the Standard Webhooks default). */
23
+ export const MAX_TIMESTAMP_SKEW_SECONDS = 5 * 60;
24
+ export function secretKeyBytes(secret) {
25
+ const encoded = secret.startsWith(SECRET_PREFIX) ? secret.slice(SECRET_PREFIX.length) : secret;
26
+ try {
27
+ const decoded = Buffer.from(encoded, "base64");
28
+ // Round-trip guard: the backend uses `base64_decode($encoded, true) ?: $secret`,
29
+ // i.e. undecodable input falls back to the raw secret string.
30
+ if (decoded.length > 0 && decoded.toString("base64").replace(/=+$/, "") === encoded.replace(/=+$/, "")) {
31
+ return decoded;
32
+ }
33
+ return Buffer.from(secret, "utf8");
34
+ }
35
+ catch {
36
+ return Buffer.from(secret, "utf8");
37
+ }
38
+ }
39
+ /** One `v1,<base64>` signature for one secret — mirrors `WebhookSigner::sign()`. */
40
+ export function signQueekPayload(eventId, timestamp, body, secret) {
41
+ const mac = createHmac("sha256", secretKeyBytes(secret))
42
+ .update(`${eventId}.${timestamp}.${body}`, "utf8")
43
+ .digest();
44
+ return `v1,${mac.toString("base64")}`;
45
+ }
46
+ export function verifyQueekSignature(input, options = {}) {
47
+ return verifyQueekSignatureDetailed(input, options).ok;
48
+ }
49
+ export function verifyQueekSignatureDetailed(input, options = {}) {
50
+ const { id, timestamp, body, signatureHeader, secret } = input;
51
+ if (!id || !timestamp || !signatureHeader || !secret) {
52
+ return { ok: false, reason: "missing_headers" };
53
+ }
54
+ if (!options.skipFreshnessCheck) {
55
+ const ts = Number(timestamp);
56
+ const now = options.nowSeconds ?? Math.floor(Date.now() / 1000);
57
+ const maxSkew = options.maxSkewSeconds ?? MAX_TIMESTAMP_SKEW_SECONDS;
58
+ if (!Number.isFinite(ts) || Math.abs(now - ts) > maxSkew) {
59
+ return { ok: false, reason: "stale_timestamp" };
60
+ }
61
+ }
62
+ const expected = signQueekPayload(id, timestamp, body, secret);
63
+ const expectedBuf = Buffer.from(expected, "utf8");
64
+ for (const candidate of signatureHeader.trim().split(/\s+/)) {
65
+ if (candidate === "")
66
+ continue;
67
+ const candidateBuf = Buffer.from(candidate, "utf8");
68
+ if (candidateBuf.length === expectedBuf.length && timingSafeEqual(candidateBuf, expectedBuf)) {
69
+ return { ok: true };
70
+ }
71
+ }
72
+ return { ok: false, reason: "signature_mismatch" };
73
+ }
74
+ //# sourceMappingURL=signatures.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"signatures.js","sourceRoot":"","sources":["../src/signatures.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAE1D;;;;;;;;;;;;;;;GAeG;AAEH,MAAM,CAAC,MAAM,iBAAiB,GAAG,YAAY,CAAC;AAC9C,MAAM,CAAC,MAAM,wBAAwB,GAAG,mBAAmB,CAAC;AAC5D,MAAM,CAAC,MAAM,wBAAwB,GAAG,mBAAmB,CAAC;AAC5D,MAAM,CAAC,MAAM,aAAa,GAAG,QAAQ,CAAC;AAEtC,2GAA2G;AAC3G,MAAM,CAAC,MAAM,0BAA0B,GAAG,CAAC,GAAG,EAAE,CAAC;AAwBjD,MAAM,UAAU,cAAc,CAAC,MAAc;IAC3C,MAAM,OAAO,GAAG,MAAM,CAAC,UAAU,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,aAAa,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC;IAC/F,IAAI,CAAC;QACH,MAAM,OAAO,GAAG,MAAM,CAAC,IAAI,CAAC,OAAO,EAAE,QAAQ,CAAC,CAAC;QAC/C,iFAAiF;QACjF,8DAA8D;QAC9D,IAAI,OAAO,CAAC,MAAM,GAAG,CAAC,IAAI,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,OAAO,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,EAAE,CAAC;YACvG,OAAO,OAAO,CAAC;QACjB,CAAC;QACD,OAAO,MAAM,CAAC,IAAI,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACrC,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,MAAM,CAAC,IAAI,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACrC,CAAC;AACH,CAAC;AAED,oFAAoF;AACpF,MAAM,UAAU,gBAAgB,CAC9B,OAAe,EACf,SAA0B,EAC1B,IAAY,EACZ,MAAc;IAEd,MAAM,GAAG,GAAG,UAAU,CAAC,QAAQ,EAAE,cAAc,CAAC,MAAM,CAAC,CAAC;SACrD,MAAM,CAAC,GAAG,OAAO,IAAI,SAAS,IAAI,IAAI,EAAE,EAAE,MAAM,CAAC;SACjD,MAAM,EAAE,CAAC;IACZ,OAAO,MAAM,GAAG,CAAC,QAAQ,CAAC,QAAQ,CAAC,EAAE,CAAC;AACxC,CAAC;AAKD,MAAM,UAAU,oBAAoB,CAAC,KAAqB,EAAE,UAAyB,EAAE;IACrF,OAAO,4BAA4B,CAAC,KAAK,EAAE,OAAO,CAAC,CAAC,EAAE,CAAC;AACzD,CAAC;AAED,MAAM,UAAU,4BAA4B,CAC1C,KAAqB,EACrB,UAAyB,EAAE;IAE3B,MAAM,EAAE,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,eAAe,EAAE,MAAM,EAAE,GAAG,KAAK,CAAC;IAC/D,IAAI,CAAC,EAAE,IAAI,CAAC,SAAS,IAAI,CAAC,eAAe,IAAI,CAAC,MAAM,EAAE,CAAC;QACrD,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,iBAAiB,EAAE,CAAC;IAClD,CAAC;IAED,IAAI,CAAC,OAAO,CAAC,kBAAkB,EAAE,CAAC;QAChC,MAAM,EAAE,GAAG,MAAM,CAAC,SAAS,CAAC,CAAC;QAC7B,MAAM,GAAG,GAAG,OAAO,CAAC,UAAU,IAAI,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC,CAAC;QAChE,MAAM,OAAO,GAAG,OAAO,CAAC,cAAc,IAAI,0BAA0B,CAAC;QACrE,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,EAAE,CAAC,IAAI,IAAI,CAAC,GAAG,CAAC,GAAG,GAAG,EAAE,CAAC,GAAG,OAAO,EAAE,CAAC;YACzD,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,iBAAiB,EAAE,CAAC;QAClD,CAAC;IACH,CAAC;IAED,MAAM,QAAQ,GAAG,gBAAgB,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,MAAM,CAAC,CAAC;IAC/D,MAAM,WAAW,GAAG,MAAM,CAAC,IAAI,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC;IAClD,KAAK,MAAM,SAAS,IAAI,eAAe,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,KAAK,CAAC,EAAE,CAAC;QAC5D,IAAI,SAAS,KAAK,EAAE;YAAE,SAAS;QAC/B,MAAM,YAAY,GAAG,MAAM,CAAC,IAAI,CAAC,SAAS,EAAE,MAAM,CAAC,CAAC;QACpD,IAAI,YAAY,CAAC,MAAM,KAAK,WAAW,CAAC,MAAM,IAAI,eAAe,CAAC,YAAY,EAAE,WAAW,CAAC,EAAE,CAAC;YAC7F,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,CAAC;QACtB,CAAC;IACH,CAAC;IACD,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,oBAAoB,EAAE,CAAC;AACrD,CAAC"}