@tappify/extension-sdk 2.1.0-beta.2 → 2.1.0-beta.4

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 (66) hide show
  1. package/dist/{chunk-QJMZRV7A.mjs → chunk-6FASZFC4.mjs} +19 -7
  2. package/dist/chunk-6FASZFC4.mjs.map +1 -0
  3. package/dist/{chunk-EB5XWQAU.cjs → chunk-6MP5DKLZ.cjs} +28 -16
  4. package/dist/chunk-6MP5DKLZ.cjs.map +1 -0
  5. package/dist/{chunk-BPCGXXMD.cjs → chunk-AYYJCRS4.cjs} +10 -10
  6. package/dist/{chunk-BPCGXXMD.cjs.map → chunk-AYYJCRS4.cjs.map} +1 -1
  7. package/dist/{chunk-KZ5NGFUP.mjs → chunk-CEFLOQD4.mjs} +2 -2
  8. package/dist/{chunk-7YKH4LZN.cjs → chunk-EV3KKP62.cjs} +17 -6
  9. package/dist/chunk-EV3KKP62.cjs.map +1 -0
  10. package/dist/{chunk-SGCYEEXD.mjs → chunk-IHLK4I7J.mjs} +17 -6
  11. package/dist/chunk-IHLK4I7J.mjs.map +1 -0
  12. package/dist/{chunk-A74HCO6T.cjs → chunk-TEIGGORZ.cjs} +3 -3
  13. package/dist/{chunk-A74HCO6T.cjs.map → chunk-TEIGGORZ.cjs.map} +1 -1
  14. package/dist/{chunk-SWG7CNTZ.mjs → chunk-WW2D3N2T.mjs} +3 -3
  15. package/dist/{client-DE-vMgdO.d.cts → client-BLnec4SD.d.cts} +1 -1
  16. package/dist/{client-BO3kavn0.d.ts → client-BRxKTo-L.d.ts} +1 -1
  17. package/dist/codegen.d.cts +14 -5
  18. package/dist/codegen.d.ts +14 -5
  19. package/dist/{constants-Bh759FUI.d.cts → constants-CvHbO3ia.d.cts} +14 -5
  20. package/dist/{constants-Bh759FUI.d.ts → constants-CvHbO3ia.d.ts} +14 -5
  21. package/dist/{context-mr8R5CNt.d.ts → context-BjhW0QWy.d.ts} +1 -1
  22. package/dist/{context-DI0EJCRD.d.cts → context-CsviWecX.d.cts} +1 -1
  23. package/dist/host.d.cts +4 -4
  24. package/dist/host.d.ts +4 -4
  25. package/dist/index.d.cts +4 -4
  26. package/dist/index.d.ts +4 -4
  27. package/dist/manifest.cjs +16 -16
  28. package/dist/manifest.d.cts +10 -4
  29. package/dist/manifest.d.ts +10 -4
  30. package/dist/manifest.mjs +1 -1
  31. package/dist/server.cjs +8 -8
  32. package/dist/server.d.cts +5 -5
  33. package/dist/server.d.ts +5 -5
  34. package/dist/server.mjs +3 -3
  35. package/dist/testing/index.cjs +8 -8
  36. package/dist/testing/index.d.cts +3 -3
  37. package/dist/testing/index.d.ts +3 -3
  38. package/dist/testing/index.mjs +4 -4
  39. package/dist/{types-BdDaLnqs.d.cts → types-C5k8VwaA.d.cts} +1 -1
  40. package/dist/{types-uGHrRaOj.d.ts → types-DBpDxGlT.d.ts} +1 -1
  41. package/dist/ui.d.cts +1 -1
  42. package/dist/ui.d.ts +1 -1
  43. package/dist/vite-preview-bootstrap.d.cts +2 -2
  44. package/dist/vite-preview-bootstrap.d.ts +2 -2
  45. package/dist/vite-preview.cjs +7 -7
  46. package/dist/vite-preview.cjs.map +1 -1
  47. package/dist/vite-preview.d.cts +3 -3
  48. package/dist/vite-preview.d.ts +3 -3
  49. package/dist/vite-preview.mjs +4 -4
  50. package/dist/vite-preview.mjs.map +1 -1
  51. package/dist/vite.cjs +16 -5
  52. package/dist/vite.cjs.map +1 -1
  53. package/dist/vite.d.cts +14 -5
  54. package/dist/vite.d.ts +14 -5
  55. package/dist/vite.mjs +16 -5
  56. package/dist/vite.mjs.map +1 -1
  57. package/dist/{work-events-BlOVpK0k.d.ts → work-events-BYU25wmX.d.ts} +40 -8
  58. package/dist/{work-events-D97EoqvS.d.cts → work-events-CxFu-YZE.d.cts} +40 -8
  59. package/package.json +1 -1
  60. package/schema/extension-v2.json +75 -330
  61. package/dist/chunk-7YKH4LZN.cjs.map +0 -1
  62. package/dist/chunk-EB5XWQAU.cjs.map +0 -1
  63. package/dist/chunk-QJMZRV7A.mjs.map +0 -1
  64. package/dist/chunk-SGCYEEXD.mjs.map +0 -1
  65. /package/dist/{chunk-KZ5NGFUP.mjs.map → chunk-CEFLOQD4.mjs.map} +0 -0
  66. /package/dist/{chunk-SWG7CNTZ.mjs.map → chunk-WW2D3N2T.mjs.map} +0 -0
@@ -3,7 +3,7 @@ import {
3
3
  readRecord,
4
4
  readText,
5
5
  readUnknown
6
- } from "./chunk-KZ5NGFUP.mjs";
6
+ } from "./chunk-CEFLOQD4.mjs";
7
7
  import {
8
8
  TapServerError
9
9
  } from "./chunk-QXD7WVKK.mjs";
@@ -68,6 +68,20 @@ async function sendEvent(name, payload, options) {
68
68
  function nonempty(value) {
69
69
  return typeof value === "string" && value.trim().length > 0;
70
70
  }
71
+ function validPerson(person) {
72
+ const email = readUnknown(person, "email");
73
+ return nonempty(readUnknown(person, "id")) && nonempty(readUnknown(person, "displayName")) && (email === void 0 || nonempty(email) && email === email.trim() && email.split("@").length === 2);
74
+ }
75
+ function validSegment(segment) {
76
+ const type = readUnknown(segment, "type");
77
+ if (type === "text") return typeof readUnknown(segment, "text") === "string";
78
+ return type === "user_mention" && validPerson(readUnknown(segment, "person")) && nonempty(readUnknown(segment, "fallback"));
79
+ }
80
+ function validComment(comment) {
81
+ const author = readUnknown(comment, "author");
82
+ const segments = readUnknown(comment, "segments");
83
+ return nonempty(readUnknown(comment, "externalCommentId")) && typeof readUnknown(comment, "body") === "string" && nonempty(readUnknown(comment, "createdAt")) && validPerson(author) && (segments === void 0 ? readUnknown(author, "email") === void 0 : Array.isArray(segments) && Array.from(segments).every(validSegment));
84
+ }
71
85
  function validEvent(event) {
72
86
  const type = readText(event, "type");
73
87
  if (!["comment.created", "status.changed", "item.deleted"].includes(type ?? ""))
@@ -84,12 +98,10 @@ function validEvent(event) {
84
98
  if (type === "item.deleted")
85
99
  return comment === void 0 && status === void 0;
86
100
  if (type === "status.changed") {
87
- const normalized2 = readRecord(event, "status");
88
- return comment === void 0 && nonempty(normalized2.id) && nonempty(normalized2.label) && typeof normalized2.stage === "string" && ["open", "in_progress", "done"].includes(normalized2.stage);
101
+ const normalized = readRecord(event, "status");
102
+ return comment === void 0 && nonempty(normalized.id) && nonempty(normalized.label) && typeof normalized.stage === "string" && ["open", "in_progress", "done"].includes(normalized.stage);
89
103
  }
90
- const normalized = readRecord(event, "comment");
91
- const author = readRecord(normalized, "author");
92
- return status === void 0 && nonempty(normalized.externalCommentId) && typeof normalized.body === "string" && nonempty(normalized.createdAt) && nonempty(author.id) && nonempty(author.displayName);
104
+ return status === void 0 && validComment(comment);
93
105
  }
94
106
  async function sendWorkEvent(event, options) {
95
107
  if (!validEvent(event))
@@ -129,4 +141,4 @@ export {
129
141
  sendEvent,
130
142
  sendWorkEvent
131
143
  };
132
- //# sourceMappingURL=chunk-QJMZRV7A.mjs.map
144
+ //# sourceMappingURL=chunk-6FASZFC4.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/server/events.ts","../src/server/work-events.ts"],"sourcesContent":["import { TapServerError } from '../client/errors';\nimport type { WebhookName, WebhookPayload } from '../client/types';\nimport { SERVER_ERROR_CODES } from './codes';\nimport { readRecord, readText } from './wire';\n\n/** The body `sendEvent` posts to Tappify, and the body the signature covers. */\nexport interface WebhookEnvelope {\n /** The declared webhook name. */\n event: string;\n /** The extension id the event came from. */\n source: string;\n /** When the event happened, as an ISO timestamp. */\n occurredAt: string;\n /** The build the event belongs to, when it belongs to one. */\n build?: string;\n /** The key Tappify deduplicates on, so a retry is not counted twice. */\n dedupeKey: string;\n payload: Record<string, unknown>;\n}\n\n/** The third argument of `sendEvent`. */\nexport interface SendEventOptions {\n /** Which install the event belongs to. */\n installId: string;\n extensionId: string;\n /** The inbound signing secret from the extension's Server page. */\n secret: string;\n /** A full url to post to, which replaces `baseUrl` and the built path. */\n endpoint?: string;\n /** The Tappify origin to build the path under. Defaults to production. */\n baseUrl?: string;\n /** Defaults to a fresh UUID, so give your own to make a retry idempotent. */\n dedupeKey?: string;\n build?: string;\n /** Defaults to now, in ISO form. */\n occurredAt?: string;\n /** A `fetch` of your own, for a test or a proxy. Defaults to the global one. */\n fetch?: typeof fetch;\n}\n\nconst DEFAULT_BASE_URL = 'https://api.tappify.ai';\n\nfunction toHex(buffer: ArrayBuffer): string {\n return Array.from(new Uint8Array(buffer))\n .map(byte => byte.toString(16).padStart(2, '0'))\n .join('');\n}\n\n/**\n * Returns the hex HMAC-SHA-256 of a raw body under a shared secret.\n *\n * @remarks\n * This is the value `sendEvent` sends as `X-Tappify-Signature`, computed over the\n * exact bytes of the body it posts. Call it directly only when you build the\n * request yourself, and sign the serialised string you send rather than\n * re-serialising the object, because a different key order is a different\n * signature. Uses Web Crypto, so it runs on Node, Cloudflare Workers and Vercel\n * alike.\n *\n * @example\n * ```ts\n * import { signWebhook } from '@tappify/extension-sdk/server';\n *\n * async function signed(body: string): Promise<Record<string, string>> {\n * return { 'x-tappify-signature': await signWebhook('whsec_test', body) };\n * }\n * ```\n */\nexport async function signWebhook(\n secret: string,\n body: string,\n): Promise<string> {\n const encoder = new TextEncoder();\n const key = await crypto.subtle.importKey(\n 'raw',\n encoder.encode(secret),\n { name: 'HMAC', hash: 'SHA-256' },\n false,\n ['sign'],\n );\n const signature = await crypto.subtle.sign('HMAC', key, encoder.encode(body));\n return toHex(signature);\n}\n\nasync function rejectionCode(response: Response): Promise<string | null> {\n try {\n const payload: unknown = await response.json();\n return readText(readRecord(payload, 'error'), 'code');\n } catch {\n return null;\n }\n}\n\n/**\n * Posts one of your declared webhook events to Tappify, signed with the install's\n * inbound secret.\n *\n * @remarks\n * The name and the payload are narrowed by the `TapWebhookMap` augmentation\n * `tappify extension types` writes. It builds the envelope, signs the serialised\n * body and posts it to the install's hook path under `baseUrl`, or to `endpoint`\n * when you give one. A response Tappify did not accept rejects with a\n * `TapServerError` whose `code` is always `TAP_WEBHOOK_REJECTED`, carrying the HTTP\n * status, and with Tappify's own code in the message text rather than in `code`;\n * the two usual causes are the wrong signing\n * secret and an event the manifest does not declare. Pass your own `dedupeKey` to\n * make a retry idempotent, because the default is a fresh UUID.\n *\n * @example\n * ```ts\n * import { sendEvent } from '@tappify/extension-sdk/server';\n *\n * async function reportAnomaly(installId: string, why: string): Promise<void> {\n * await sendEvent('anomaly.detected', { why }, {\n * installId,\n * extensionId: 'starter',\n * secret: process.env.TAPPIFY_INBOUND_SECRET ?? '',\n * dedupeKey: `anomaly-${installId}-${why}`,\n * });\n * }\n * ```\n */\nexport async function sendEvent<E extends WebhookName>(\n name: E,\n payload: WebhookPayload<E>,\n options: SendEventOptions,\n): Promise<void> {\n const envelope: WebhookEnvelope = {\n event: name,\n source: options.extensionId,\n occurredAt: options.occurredAt ?? new Date().toISOString(),\n dedupeKey: options.dedupeKey ?? crypto.randomUUID(),\n payload: { ...payload },\n };\n\n if (options.build !== undefined) envelope.build = options.build;\n\n const body = JSON.stringify(envelope);\n const signature = await signWebhook(options.secret, body);\n const url =\n options.endpoint ??\n `${options.baseUrl ?? DEFAULT_BASE_URL}/api/v1/extensions/hooks/${options.extensionId}/${options.installId}`;\n\n const send = options.fetch ?? fetch;\n const response = await send(url, {\n method: 'POST',\n headers: {\n 'content-type': 'application/json',\n 'x-tappify-signature': signature,\n },\n body,\n });\n\n if (!response.ok) {\n const rejection = await rejectionCode(response);\n throw new TapServerError(\n SERVER_ERROR_CODES.WEBHOOK_REJECTED,\n `Tappify answered ${String(response.status)}${rejection === null ? '' : ` (${rejection})`} for ${name}. Check the inbound signing secret on the extension's Server page, and that the event is declared under contributes.webhooks.`,\n response.status,\n );\n }\n}\n","import { TapServerError } from '../client/errors';\nimport { SERVER_ERROR_CODES } from './codes';\nimport { signWebhook } from './events';\nimport { readRecord, readText, readUnknown } from './wire';\nimport type { WorkHydratedEvent, WorkHydratedEventBase } from './work';\n\n/** An opaque provider signal to hydrate using current install credentials. */\nexport interface WorkHydrationReferenceEvent extends WorkHydratedEventBase {\n type: WorkHydratedEvent['type'];\n /** A nonempty, short-lived reference resolved by the hydrate_event handler. */\n hydrateRef: string;\n /** Normalized comment content must not accompany a reference. */\n comment?: never;\n /** Normalized status content must not accompany a reference. */\n status?: never;\n}\n\n/** Complete normalized content or an opaque reference, with stable retry identity. */\nexport type WorkEvent =\n | (WorkHydratedEvent & { hydrateRef?: never })\n | WorkHydrationReferenceEvent;\n\n/** Install-specific delivery values provisioned through work configure. */\nexport interface SendWorkEventOptions {\n /** The exact provisioned callback URL; no production default is supplied. */\n callbackUrl: string;\n /** The per-install work-event signing secret provisioned with the callback. */\n secret: string;\n /** A transport for tests or a proxy; defaults to global fetch. */\n fetch?: typeof fetch;\n}\n\nfunction nonempty(value: unknown): value is string {\n return typeof value === 'string' && value.trim().length > 0;\n}\n\nfunction validPerson(person: unknown): boolean {\n const email = readUnknown(person, 'email');\n return (\n nonempty(readUnknown(person, 'id')) &&\n nonempty(readUnknown(person, 'displayName')) &&\n (email === undefined ||\n (nonempty(email) &&\n email === email.trim() &&\n email.split('@').length === 2))\n );\n}\n\nfunction validSegment(segment: unknown): boolean {\n const type = readUnknown(segment, 'type');\n if (type === 'text') return typeof readUnknown(segment, 'text') === 'string';\n return (\n type === 'user_mention' &&\n validPerson(readUnknown(segment, 'person')) &&\n nonempty(readUnknown(segment, 'fallback'))\n );\n}\n\nfunction validComment(comment: unknown): boolean {\n const author = readUnknown(comment, 'author');\n const segments = readUnknown(comment, 'segments');\n return (\n nonempty(readUnknown(comment, 'externalCommentId')) &&\n typeof readUnknown(comment, 'body') === 'string' &&\n nonempty(readUnknown(comment, 'createdAt')) &&\n validPerson(author) &&\n (segments === undefined\n ? readUnknown(author, 'email') === undefined\n : Array.isArray(segments) && Array.from(segments).every(validSegment))\n );\n}\n\nfunction validEvent(event: WorkEvent): boolean {\n const type = readText(event, 'type');\n if (\n !['comment.created', 'status.changed', 'item.deleted'].includes(type ?? '')\n )\n return false;\n if (\n !['externalItemId', 'providerEventId', 'occurredAt', 'dedupeKey'].every(\n key => nonempty(readUnknown(event, key)),\n )\n )\n return false;\n\n const reference = readUnknown(event, 'hydrateRef');\n const comment = readUnknown(event, 'comment');\n const status = readUnknown(event, 'status');\n if (reference !== undefined)\n return nonempty(reference) && comment === undefined && status === undefined;\n\n if (type === 'item.deleted')\n return comment === undefined && status === undefined;\n if (type === 'status.changed') {\n const normalized = readRecord(event, 'status');\n return (\n comment === undefined &&\n nonempty(normalized.id) &&\n nonempty(normalized.label) &&\n typeof normalized.stage === 'string' &&\n ['open', 'in_progress', 'done'].includes(normalized.stage)\n );\n }\n\n return status === undefined && validComment(comment);\n}\n\n/**\n * Sends one provider work event to the callback provisioned for this install.\n *\n * @remarks\n * Pass the callbackUrl and secret received through configure unchanged. The\n * event's required dedupeKey identifies the logical provider signal and must be\n * reused on retries. Complete normalized content and an opaque hydrateRef are\n * mutually exclusive. The exact serialized UTF-8 body is signed with HMAC-SHA-256.\n * This callback is independent of declared alert and banner webhooks.\n */\nexport async function sendWorkEvent(\n event: WorkEvent,\n options: SendWorkEventOptions,\n): Promise<void> {\n if (!validEvent(event))\n throw new TypeError(\n 'Invalid work event: provide complete normalized content or a nonempty hydrateRef, with routing and identity fields.',\n );\n if (!nonempty(options.callbackUrl))\n throw new TypeError(\n 'A work event requires the callbackUrl provisioned through configure.',\n );\n\n const body = JSON.stringify(event);\n const signature = await signWebhook(options.secret, body);\n const response = await (options.fetch ?? fetch)(options.callbackUrl, {\n method: 'POST',\n headers: {\n 'content-type': 'application/json',\n 'x-tappify-signature': signature,\n },\n body,\n });\n\n if (!response.ok) {\n let rejection: string | null = null;\n try {\n rejection = readText(readRecord(await response.json(), 'error'), 'code');\n } catch {\n // Proxies may return a non-JSON rejection body.\n }\n throw new TapServerError(\n SERVER_ERROR_CODES.WEBHOOK_REJECTED,\n `Tappify answered ${String(response.status)}${rejection === null ? '' : ` (${rejection})`} for work event ${event.type}. Check the callback URL and per-install work-event signing secret provisioned through configure.`,\n response.status,\n );\n }\n}\n"],"mappings":"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","names":[]}
@@ -3,7 +3,7 @@
3
3
 
4
4
 
5
5
 
6
- var _chunkA74HCO6Tcjs = require('./chunk-A74HCO6T.cjs');
6
+ var _chunkTEIGGORZcjs = require('./chunk-TEIGGORZ.cjs');
7
7
 
8
8
 
9
9
  var _chunk6HETNZJYcjs = require('./chunk-6HETNZJY.cjs');
@@ -28,7 +28,7 @@ async function signWebhook(secret, body) {
28
28
  async function rejectionCode(response) {
29
29
  try {
30
30
  const payload = await response.json();
31
- return _chunkA74HCO6Tcjs.readText.call(void 0, _chunkA74HCO6Tcjs.readRecord.call(void 0, payload, "error"), "code");
31
+ return _chunkTEIGGORZcjs.readText.call(void 0, _chunkTEIGGORZcjs.readRecord.call(void 0, payload, "error"), "code");
32
32
  } catch (e) {
33
33
  return null;
34
34
  }
@@ -57,7 +57,7 @@ async function sendEvent(name, payload, options) {
57
57
  if (!response.ok) {
58
58
  const rejection = await rejectionCode(response);
59
59
  throw new (0, _chunk6HETNZJYcjs.TapServerError)(
60
- _chunkA74HCO6Tcjs.SERVER_ERROR_CODES.WEBHOOK_REJECTED,
60
+ _chunkTEIGGORZcjs.SERVER_ERROR_CODES.WEBHOOK_REJECTED,
61
61
  `Tappify answered ${String(response.status)}${rejection === null ? "" : ` (${rejection})`} for ${name}. Check the inbound signing secret on the extension's Server page, and that the event is declared under contributes.webhooks.`,
62
62
  response.status
63
63
  );
@@ -68,28 +68,40 @@ async function sendEvent(name, payload, options) {
68
68
  function nonempty(value) {
69
69
  return typeof value === "string" && value.trim().length > 0;
70
70
  }
71
+ function validPerson(person) {
72
+ const email = _chunkTEIGGORZcjs.readUnknown.call(void 0, person, "email");
73
+ return nonempty(_chunkTEIGGORZcjs.readUnknown.call(void 0, person, "id")) && nonempty(_chunkTEIGGORZcjs.readUnknown.call(void 0, person, "displayName")) && (email === void 0 || nonempty(email) && email === email.trim() && email.split("@").length === 2);
74
+ }
75
+ function validSegment(segment) {
76
+ const type = _chunkTEIGGORZcjs.readUnknown.call(void 0, segment, "type");
77
+ if (type === "text") return typeof _chunkTEIGGORZcjs.readUnknown.call(void 0, segment, "text") === "string";
78
+ return type === "user_mention" && validPerson(_chunkTEIGGORZcjs.readUnknown.call(void 0, segment, "person")) && nonempty(_chunkTEIGGORZcjs.readUnknown.call(void 0, segment, "fallback"));
79
+ }
80
+ function validComment(comment) {
81
+ const author = _chunkTEIGGORZcjs.readUnknown.call(void 0, comment, "author");
82
+ const segments = _chunkTEIGGORZcjs.readUnknown.call(void 0, comment, "segments");
83
+ return nonempty(_chunkTEIGGORZcjs.readUnknown.call(void 0, comment, "externalCommentId")) && typeof _chunkTEIGGORZcjs.readUnknown.call(void 0, comment, "body") === "string" && nonempty(_chunkTEIGGORZcjs.readUnknown.call(void 0, comment, "createdAt")) && validPerson(author) && (segments === void 0 ? _chunkTEIGGORZcjs.readUnknown.call(void 0, author, "email") === void 0 : Array.isArray(segments) && Array.from(segments).every(validSegment));
84
+ }
71
85
  function validEvent(event) {
72
- const type = _chunkA74HCO6Tcjs.readText.call(void 0, event, "type");
86
+ const type = _chunkTEIGGORZcjs.readText.call(void 0, event, "type");
73
87
  if (!["comment.created", "status.changed", "item.deleted"].includes(_nullishCoalesce(type, () => ( ""))))
74
88
  return false;
75
89
  if (!["externalItemId", "providerEventId", "occurredAt", "dedupeKey"].every(
76
- (key) => nonempty(_chunkA74HCO6Tcjs.readUnknown.call(void 0, event, key))
90
+ (key) => nonempty(_chunkTEIGGORZcjs.readUnknown.call(void 0, event, key))
77
91
  ))
78
92
  return false;
79
- const reference = _chunkA74HCO6Tcjs.readUnknown.call(void 0, event, "hydrateRef");
80
- const comment = _chunkA74HCO6Tcjs.readUnknown.call(void 0, event, "comment");
81
- const status = _chunkA74HCO6Tcjs.readUnknown.call(void 0, event, "status");
93
+ const reference = _chunkTEIGGORZcjs.readUnknown.call(void 0, event, "hydrateRef");
94
+ const comment = _chunkTEIGGORZcjs.readUnknown.call(void 0, event, "comment");
95
+ const status = _chunkTEIGGORZcjs.readUnknown.call(void 0, event, "status");
82
96
  if (reference !== void 0)
83
97
  return nonempty(reference) && comment === void 0 && status === void 0;
84
98
  if (type === "item.deleted")
85
99
  return comment === void 0 && status === void 0;
86
100
  if (type === "status.changed") {
87
- const normalized2 = _chunkA74HCO6Tcjs.readRecord.call(void 0, event, "status");
88
- return comment === void 0 && nonempty(normalized2.id) && nonempty(normalized2.label) && typeof normalized2.stage === "string" && ["open", "in_progress", "done"].includes(normalized2.stage);
101
+ const normalized = _chunkTEIGGORZcjs.readRecord.call(void 0, event, "status");
102
+ return comment === void 0 && nonempty(normalized.id) && nonempty(normalized.label) && typeof normalized.stage === "string" && ["open", "in_progress", "done"].includes(normalized.stage);
89
103
  }
90
- const normalized = _chunkA74HCO6Tcjs.readRecord.call(void 0, event, "comment");
91
- const author = _chunkA74HCO6Tcjs.readRecord.call(void 0, normalized, "author");
92
- return status === void 0 && nonempty(normalized.externalCommentId) && typeof normalized.body === "string" && nonempty(normalized.createdAt) && nonempty(author.id) && nonempty(author.displayName);
104
+ return status === void 0 && validComment(comment);
93
105
  }
94
106
  async function sendWorkEvent(event, options) {
95
107
  if (!validEvent(event))
@@ -113,11 +125,11 @@ async function sendWorkEvent(event, options) {
113
125
  if (!response.ok) {
114
126
  let rejection = null;
115
127
  try {
116
- rejection = _chunkA74HCO6Tcjs.readText.call(void 0, _chunkA74HCO6Tcjs.readRecord.call(void 0, await response.json(), "error"), "code");
128
+ rejection = _chunkTEIGGORZcjs.readText.call(void 0, _chunkTEIGGORZcjs.readRecord.call(void 0, await response.json(), "error"), "code");
117
129
  } catch (e2) {
118
130
  }
119
131
  throw new (0, _chunk6HETNZJYcjs.TapServerError)(
120
- _chunkA74HCO6Tcjs.SERVER_ERROR_CODES.WEBHOOK_REJECTED,
132
+ _chunkTEIGGORZcjs.SERVER_ERROR_CODES.WEBHOOK_REJECTED,
121
133
  `Tappify answered ${String(response.status)}${rejection === null ? "" : ` (${rejection})`} for work event ${event.type}. Check the callback URL and per-install work-event signing secret provisioned through configure.`,
122
134
  response.status
123
135
  );
@@ -129,4 +141,4 @@ async function sendWorkEvent(event, options) {
129
141
 
130
142
 
131
143
  exports.signWebhook = signWebhook; exports.sendEvent = sendEvent; exports.sendWorkEvent = sendWorkEvent;
132
- //# sourceMappingURL=chunk-EB5XWQAU.cjs.map
144
+ //# sourceMappingURL=chunk-6MP5DKLZ.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["/home/runner/work/extension-sdk/extension-sdk/dist/chunk-6MP5DKLZ.cjs","../src/server/events.ts","../src/server/work-events.ts"],"names":[],"mappings":"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","file":"/home/runner/work/extension-sdk/extension-sdk/dist/chunk-6MP5DKLZ.cjs","sourcesContent":[null,"import { TapServerError } from '../client/errors';\nimport type { WebhookName, WebhookPayload } from '../client/types';\nimport { SERVER_ERROR_CODES } from './codes';\nimport { readRecord, readText } from './wire';\n\n/** The body `sendEvent` posts to Tappify, and the body the signature covers. */\nexport interface WebhookEnvelope {\n /** The declared webhook name. */\n event: string;\n /** The extension id the event came from. */\n source: string;\n /** When the event happened, as an ISO timestamp. */\n occurredAt: string;\n /** The build the event belongs to, when it belongs to one. */\n build?: string;\n /** The key Tappify deduplicates on, so a retry is not counted twice. */\n dedupeKey: string;\n payload: Record<string, unknown>;\n}\n\n/** The third argument of `sendEvent`. */\nexport interface SendEventOptions {\n /** Which install the event belongs to. */\n installId: string;\n extensionId: string;\n /** The inbound signing secret from the extension's Server page. */\n secret: string;\n /** A full url to post to, which replaces `baseUrl` and the built path. */\n endpoint?: string;\n /** The Tappify origin to build the path under. Defaults to production. */\n baseUrl?: string;\n /** Defaults to a fresh UUID, so give your own to make a retry idempotent. */\n dedupeKey?: string;\n build?: string;\n /** Defaults to now, in ISO form. */\n occurredAt?: string;\n /** A `fetch` of your own, for a test or a proxy. Defaults to the global one. */\n fetch?: typeof fetch;\n}\n\nconst DEFAULT_BASE_URL = 'https://api.tappify.ai';\n\nfunction toHex(buffer: ArrayBuffer): string {\n return Array.from(new Uint8Array(buffer))\n .map(byte => byte.toString(16).padStart(2, '0'))\n .join('');\n}\n\n/**\n * Returns the hex HMAC-SHA-256 of a raw body under a shared secret.\n *\n * @remarks\n * This is the value `sendEvent` sends as `X-Tappify-Signature`, computed over the\n * exact bytes of the body it posts. Call it directly only when you build the\n * request yourself, and sign the serialised string you send rather than\n * re-serialising the object, because a different key order is a different\n * signature. Uses Web Crypto, so it runs on Node, Cloudflare Workers and Vercel\n * alike.\n *\n * @example\n * ```ts\n * import { signWebhook } from '@tappify/extension-sdk/server';\n *\n * async function signed(body: string): Promise<Record<string, string>> {\n * return { 'x-tappify-signature': await signWebhook('whsec_test', body) };\n * }\n * ```\n */\nexport async function signWebhook(\n secret: string,\n body: string,\n): Promise<string> {\n const encoder = new TextEncoder();\n const key = await crypto.subtle.importKey(\n 'raw',\n encoder.encode(secret),\n { name: 'HMAC', hash: 'SHA-256' },\n false,\n ['sign'],\n );\n const signature = await crypto.subtle.sign('HMAC', key, encoder.encode(body));\n return toHex(signature);\n}\n\nasync function rejectionCode(response: Response): Promise<string | null> {\n try {\n const payload: unknown = await response.json();\n return readText(readRecord(payload, 'error'), 'code');\n } catch {\n return null;\n }\n}\n\n/**\n * Posts one of your declared webhook events to Tappify, signed with the install's\n * inbound secret.\n *\n * @remarks\n * The name and the payload are narrowed by the `TapWebhookMap` augmentation\n * `tappify extension types` writes. It builds the envelope, signs the serialised\n * body and posts it to the install's hook path under `baseUrl`, or to `endpoint`\n * when you give one. A response Tappify did not accept rejects with a\n * `TapServerError` whose `code` is always `TAP_WEBHOOK_REJECTED`, carrying the HTTP\n * status, and with Tappify's own code in the message text rather than in `code`;\n * the two usual causes are the wrong signing\n * secret and an event the manifest does not declare. Pass your own `dedupeKey` to\n * make a retry idempotent, because the default is a fresh UUID.\n *\n * @example\n * ```ts\n * import { sendEvent } from '@tappify/extension-sdk/server';\n *\n * async function reportAnomaly(installId: string, why: string): Promise<void> {\n * await sendEvent('anomaly.detected', { why }, {\n * installId,\n * extensionId: 'starter',\n * secret: process.env.TAPPIFY_INBOUND_SECRET ?? '',\n * dedupeKey: `anomaly-${installId}-${why}`,\n * });\n * }\n * ```\n */\nexport async function sendEvent<E extends WebhookName>(\n name: E,\n payload: WebhookPayload<E>,\n options: SendEventOptions,\n): Promise<void> {\n const envelope: WebhookEnvelope = {\n event: name,\n source: options.extensionId,\n occurredAt: options.occurredAt ?? new Date().toISOString(),\n dedupeKey: options.dedupeKey ?? crypto.randomUUID(),\n payload: { ...payload },\n };\n\n if (options.build !== undefined) envelope.build = options.build;\n\n const body = JSON.stringify(envelope);\n const signature = await signWebhook(options.secret, body);\n const url =\n options.endpoint ??\n `${options.baseUrl ?? DEFAULT_BASE_URL}/api/v1/extensions/hooks/${options.extensionId}/${options.installId}`;\n\n const send = options.fetch ?? fetch;\n const response = await send(url, {\n method: 'POST',\n headers: {\n 'content-type': 'application/json',\n 'x-tappify-signature': signature,\n },\n body,\n });\n\n if (!response.ok) {\n const rejection = await rejectionCode(response);\n throw new TapServerError(\n SERVER_ERROR_CODES.WEBHOOK_REJECTED,\n `Tappify answered ${String(response.status)}${rejection === null ? '' : ` (${rejection})`} for ${name}. Check the inbound signing secret on the extension's Server page, and that the event is declared under contributes.webhooks.`,\n response.status,\n );\n }\n}\n","import { TapServerError } from '../client/errors';\nimport { SERVER_ERROR_CODES } from './codes';\nimport { signWebhook } from './events';\nimport { readRecord, readText, readUnknown } from './wire';\nimport type { WorkHydratedEvent, WorkHydratedEventBase } from './work';\n\n/** An opaque provider signal to hydrate using current install credentials. */\nexport interface WorkHydrationReferenceEvent extends WorkHydratedEventBase {\n type: WorkHydratedEvent['type'];\n /** A nonempty, short-lived reference resolved by the hydrate_event handler. */\n hydrateRef: string;\n /** Normalized comment content must not accompany a reference. */\n comment?: never;\n /** Normalized status content must not accompany a reference. */\n status?: never;\n}\n\n/** Complete normalized content or an opaque reference, with stable retry identity. */\nexport type WorkEvent =\n | (WorkHydratedEvent & { hydrateRef?: never })\n | WorkHydrationReferenceEvent;\n\n/** Install-specific delivery values provisioned through work configure. */\nexport interface SendWorkEventOptions {\n /** The exact provisioned callback URL; no production default is supplied. */\n callbackUrl: string;\n /** The per-install work-event signing secret provisioned with the callback. */\n secret: string;\n /** A transport for tests or a proxy; defaults to global fetch. */\n fetch?: typeof fetch;\n}\n\nfunction nonempty(value: unknown): value is string {\n return typeof value === 'string' && value.trim().length > 0;\n}\n\nfunction validPerson(person: unknown): boolean {\n const email = readUnknown(person, 'email');\n return (\n nonempty(readUnknown(person, 'id')) &&\n nonempty(readUnknown(person, 'displayName')) &&\n (email === undefined ||\n (nonempty(email) &&\n email === email.trim() &&\n email.split('@').length === 2))\n );\n}\n\nfunction validSegment(segment: unknown): boolean {\n const type = readUnknown(segment, 'type');\n if (type === 'text') return typeof readUnknown(segment, 'text') === 'string';\n return (\n type === 'user_mention' &&\n validPerson(readUnknown(segment, 'person')) &&\n nonempty(readUnknown(segment, 'fallback'))\n );\n}\n\nfunction validComment(comment: unknown): boolean {\n const author = readUnknown(comment, 'author');\n const segments = readUnknown(comment, 'segments');\n return (\n nonempty(readUnknown(comment, 'externalCommentId')) &&\n typeof readUnknown(comment, 'body') === 'string' &&\n nonempty(readUnknown(comment, 'createdAt')) &&\n validPerson(author) &&\n (segments === undefined\n ? readUnknown(author, 'email') === undefined\n : Array.isArray(segments) && Array.from(segments).every(validSegment))\n );\n}\n\nfunction validEvent(event: WorkEvent): boolean {\n const type = readText(event, 'type');\n if (\n !['comment.created', 'status.changed', 'item.deleted'].includes(type ?? '')\n )\n return false;\n if (\n !['externalItemId', 'providerEventId', 'occurredAt', 'dedupeKey'].every(\n key => nonempty(readUnknown(event, key)),\n )\n )\n return false;\n\n const reference = readUnknown(event, 'hydrateRef');\n const comment = readUnknown(event, 'comment');\n const status = readUnknown(event, 'status');\n if (reference !== undefined)\n return nonempty(reference) && comment === undefined && status === undefined;\n\n if (type === 'item.deleted')\n return comment === undefined && status === undefined;\n if (type === 'status.changed') {\n const normalized = readRecord(event, 'status');\n return (\n comment === undefined &&\n nonempty(normalized.id) &&\n nonempty(normalized.label) &&\n typeof normalized.stage === 'string' &&\n ['open', 'in_progress', 'done'].includes(normalized.stage)\n );\n }\n\n return status === undefined && validComment(comment);\n}\n\n/**\n * Sends one provider work event to the callback provisioned for this install.\n *\n * @remarks\n * Pass the callbackUrl and secret received through configure unchanged. The\n * event's required dedupeKey identifies the logical provider signal and must be\n * reused on retries. Complete normalized content and an opaque hydrateRef are\n * mutually exclusive. The exact serialized UTF-8 body is signed with HMAC-SHA-256.\n * This callback is independent of declared alert and banner webhooks.\n */\nexport async function sendWorkEvent(\n event: WorkEvent,\n options: SendWorkEventOptions,\n): Promise<void> {\n if (!validEvent(event))\n throw new TypeError(\n 'Invalid work event: provide complete normalized content or a nonempty hydrateRef, with routing and identity fields.',\n );\n if (!nonempty(options.callbackUrl))\n throw new TypeError(\n 'A work event requires the callbackUrl provisioned through configure.',\n );\n\n const body = JSON.stringify(event);\n const signature = await signWebhook(options.secret, body);\n const response = await (options.fetch ?? fetch)(options.callbackUrl, {\n method: 'POST',\n headers: {\n 'content-type': 'application/json',\n 'x-tappify-signature': signature,\n },\n body,\n });\n\n if (!response.ok) {\n let rejection: string | null = null;\n try {\n rejection = readText(readRecord(await response.json(), 'error'), 'code');\n } catch {\n // Proxies may return a non-JSON rejection body.\n }\n throw new TapServerError(\n SERVER_ERROR_CODES.WEBHOOK_REJECTED,\n `Tappify answered ${String(response.status)}${rejection === null ? '' : ` (${rejection})`} for work event ${event.type}. Check the callback URL and per-install work-event signing secret provisioned through configure.`,\n response.status,\n );\n }\n}\n"]}
@@ -3,11 +3,11 @@
3
3
 
4
4
 
5
5
 
6
- var _chunkA74HCO6Tcjs = require('./chunk-A74HCO6T.cjs');
6
+ var _chunkTEIGGORZcjs = require('./chunk-TEIGGORZ.cjs');
7
7
 
8
8
 
9
9
 
10
- var _chunk7YKH4LZNcjs = require('./chunk-7YKH4LZN.cjs');
10
+ var _chunkEV3KKP62cjs = require('./chunk-EV3KKP62.cjs');
11
11
 
12
12
 
13
13
 
@@ -275,15 +275,15 @@ async function readEnvelopeResponse(response, call) {
275
275
  if (body.parsed) return body.value;
276
276
  if (text.trim() === "") return void 0;
277
277
  throw new (0, _chunk6HETNZJYcjs.TapServerError)(
278
- _chunkA74HCO6Tcjs.SERVER_ERROR_CODES.INTERNAL_ERROR,
278
+ _chunkTEIGGORZcjs.SERVER_ERROR_CODES.INTERNAL_ERROR,
279
279
  `The extension server answered ${status} for ${call} with a body that is not JSON. Every Tappify route answers JSON, or 204 with no body.`,
280
280
  response.status
281
281
  );
282
282
  }
283
- const error = _chunkA74HCO6Tcjs.readRecord.call(void 0, body.value, "error");
283
+ const error = _chunkTEIGGORZcjs.readRecord.call(void 0, body.value, "error");
284
284
  throw new (0, _chunk6HETNZJYcjs.TapServerError)(
285
- _nullishCoalesce(_chunkA74HCO6Tcjs.readText.call(void 0, error, "code"), () => ( _chunkA74HCO6Tcjs.SERVER_ERROR_CODES.INTERNAL_ERROR)),
286
- _nullishCoalesce(_chunkA74HCO6Tcjs.readText.call(void 0, error, "message"), () => ( `The extension server answered ${status} for ${call} without a JSON error body.`)),
285
+ _nullishCoalesce(_chunkTEIGGORZcjs.readText.call(void 0, error, "code"), () => ( _chunkTEIGGORZcjs.SERVER_ERROR_CODES.INTERNAL_ERROR)),
286
+ _nullishCoalesce(_chunkTEIGGORZcjs.readText.call(void 0, error, "message"), () => ( `The extension server answered ${status} for ${call} without a JSON error body.`)),
287
287
  response.status
288
288
  );
289
289
  }
@@ -330,7 +330,7 @@ function createTestClient(target, defaults = {}) {
330
330
  }
331
331
  const extensionId = target.extensionId;
332
332
  const handlerPromise = testJwks().then(
333
- (jwks) => _chunkA74HCO6Tcjs.createTappifyHandler.call(void 0, { ...target, jwks })
333
+ (jwks) => _chunkTEIGGORZcjs.createTappifyHandler.call(void 0, { ...target, jwks })
334
334
  );
335
335
  const install = {
336
336
  id: "ins_test",
@@ -458,7 +458,7 @@ function asObject(value) {
458
458
  return typeof value === "object" && value !== null ? value : {};
459
459
  }
460
460
  function createTapMock(options = {}) {
461
- const granted = new Set(_nullishCoalesce(options.scopes, () => ( _chunk7YKH4LZNcjs.SCOPE_KEYS)));
461
+ const granted = new Set(_nullishCoalesce(options.scopes, () => ( _chunkEV3KKP62cjs.SCOPE_KEYS)));
462
462
  const listeners = /* @__PURE__ */ new Map();
463
463
  const eventHandlers = /* @__PURE__ */ new Map();
464
464
  const state = /* @__PURE__ */ new Map();
@@ -662,7 +662,7 @@ function createTapMock(options = {}) {
662
662
  },
663
663
  data: {
664
664
  query: (query) => {
665
- const scope = _chunk7YKH4LZNcjs.QUERY_SCOPES[query.kind];
665
+ const scope = _chunkEV3KKP62cjs.QUERY_SCOPES[query.kind];
666
666
  if (!granted.has(scope)) return Promise.reject(scopeError(scope));
667
667
  return Promise.resolve(answerFixtureQuery(query));
668
668
  },
@@ -778,4 +778,4 @@ function createTapMock(options = {}) {
778
778
 
779
779
 
780
780
  exports.fixtureApps = fixtureApps; exports.fixtureReleases = fixtureReleases; exports.fixtureKeywords = fixtureKeywords; exports.fixtureListings = fixtureListings; exports.fixtureProject = fixtureProject; exports.fixtureSeries = fixtureSeries; exports.fixtureReviews = fixtureReviews; exports.fixtureCrashes = fixtureCrashes; exports.fixtureRevenue = fixtureRevenue; exports.fixtures = fixtures; exports.answerFixtureQuery = answerFixtureQuery; exports.testJwks = testJwks; exports.signTestToken = signTestToken; exports.createTestClient = createTestClient; exports.createTapMock = createTapMock;
781
- //# sourceMappingURL=chunk-BPCGXXMD.cjs.map
781
+ //# sourceMappingURL=chunk-AYYJCRS4.cjs.map
@@ -1 +1 @@
1
- {"version":3,"sources":["/home/runner/work/extension-sdk/extension-sdk/dist/chunk-BPCGXXMD.cjs","../src/testing/fixtures.ts","../src/testing/client.ts","../src/testing/envelope.ts","../src/testing/mock.ts"],"names":[],"mappings":"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","file":"/home/runner/work/extension-sdk/extension-sdk/dist/chunk-BPCGXXMD.cjs","sourcesContent":[null,"import type {\n TapApp,\n TapCrashesResult,\n TapKeyword,\n TapListing,\n TapProjectResult,\n TapQuery,\n TapRelease,\n TapResult,\n TapRevenueResult,\n TapReviewsResult,\n TapSeriesMetric,\n TapSeriesPoint,\n TapSeriesResult,\n} from '../client/types';\n\n/** The owner's two Northlight apps, one per platform. */\nexport const fixtureApps: TapApp[] = [\n {\n id: 'app_ios',\n name: 'Northlight',\n platform: 'ios',\n storeId: '1490000001',\n bundleId: 'ai.tappify.northlight',\n },\n {\n id: 'app_android',\n name: 'Northlight',\n platform: 'android',\n storeId: 'ai.tappify.northlight',\n bundleId: 'ai.tappify.northlight',\n },\n];\n\n/** Three shipped releases: two iOS versions and the Android build of the newer one. */\nexport const fixtureReleases: TapRelease[] = [\n {\n id: 'rel_42',\n version: '4.2.0',\n build: '118',\n platform: 'ios',\n state: 'ready_for_sale',\n shippedAt: '2026-09-01T09:00:00.000Z',\n },\n {\n id: 'rel_41',\n version: '4.1.0',\n build: '107',\n platform: 'ios',\n state: 'ready_for_sale',\n shippedAt: '2026-08-11T09:00:00.000Z',\n },\n {\n id: 'rel_a42',\n version: '4.2.0',\n build: '4200',\n platform: 'android',\n state: 'published',\n shippedAt: '2026-09-02T09:00:00.000Z',\n },\n];\n\n/**\n * Three tracked terms with positions, two on iOS and one on Android, scored on\n * the host's 1 to 10 traffic scale.\n */\nexport const fixtureKeywords: TapKeyword[] = [\n {\n id: 'kw_1',\n term: 'photo editor',\n platform: 'ios',\n country: 'US',\n position: 4,\n popularity: 6.6,\n },\n {\n id: 'kw_2',\n term: 'collage maker',\n platform: 'ios',\n country: 'US',\n position: 11,\n popularity: 5.3,\n },\n {\n id: 'kw_3',\n term: 'photo editor',\n platform: 'android',\n country: 'US',\n position: 7,\n popularity: 6,\n },\n];\n\n/** One store listing, for iOS in `en-US`. */\nexport const fixtureListings: TapListing[] = [\n {\n platform: 'ios',\n country: 'US',\n locale: 'en-US',\n title: 'Northlight — Photo Editor',\n subtitle: 'Edit, collage, share',\n description: 'Northlight turns a camera roll into a finished post.',\n keywords: ['photo', 'editor', 'collage'],\n updatedAt: '2026-08-28T12:00:00.000Z',\n },\n];\n\n/** The Northlight project, with `fixtureApps` and `fixtureReleases` under it. */\nexport const fixtureProject: TapProjectResult = {\n project: {\n id: 'prj_north',\n name: 'Northlight',\n platforms: ['ios', 'android'],\n },\n apps: fixtureApps,\n releases: fixtureReleases,\n};\n\nconst SERIES_VALUES: Record<TapSeriesMetric, number[]> = {\n downloads: [1180, 1240, 1310, 1290, 1402, 1512, 1488],\n impressions: [21400, 22150, 22980, 22110, 24020, 25510, 25010],\n page_views: [8120, 8340, 8710, 8290, 9110, 9640, 9420],\n conversion_rate: [0.145, 0.149, 0.15, 0.156, 0.152, 0.157, 0.158],\n};\n\nfunction week(): string[] {\n return [\n '2026-09-01',\n '2026-09-02',\n '2026-09-03',\n '2026-09-04',\n '2026-09-05',\n '2026-09-06',\n '2026-09-07',\n ];\n}\n\n/**\n * Builds the seven-day series for one metric, over the first week of September\n * 2026.\n *\n * @remarks\n * The same seven values every call, so an assertion on a total or a peak is\n * stable: downloads peak at 1,512 and `conversion_rate` comes back with unit\n * `ratio` where the other three come back as `count`. The range in the query does\n * not change the points, so a test that drives the filter bar asserts on the call,\n * not on different numbers.\n *\n * @example\n * ```ts\n * import { fixtureSeries } from '@tappify/extension-sdk/testing';\n *\n * const downloads = fixtureSeries('downloads');\n * const best = Math.max(...downloads.points.map(point => point.value));\n * ```\n */\nexport function fixtureSeries(metric: TapSeriesMetric): TapSeriesResult {\n const points: TapSeriesPoint[] = week().map((at, index) => ({\n at,\n value: SERIES_VALUES[metric][index],\n }));\n\n return {\n metric,\n unit: metric === 'conversion_rate' ? 'ratio' : 'count',\n points,\n };\n}\n\n/** Three reviews across three countries, averaging 3.7 stars, one already replied to. */\nexport const fixtureReviews: TapReviewsResult = {\n total: 3,\n averageRating: 3.7,\n reviews: [\n {\n id: 'rev_1',\n platform: 'ios',\n country: 'US',\n rating: 5,\n title: 'Fast and clean',\n body: 'The new collage layouts are exactly what I wanted.',\n author: 'kbrady',\n submittedAt: '2026-09-05T18:22:00.000Z',\n replied: false,\n },\n {\n id: 'rev_2',\n platform: 'ios',\n country: 'GB',\n rating: 2,\n title: 'Crashes on export',\n body: 'Since 4.2 the export button closes the app.',\n author: null,\n submittedAt: '2026-09-04T08:10:00.000Z',\n replied: true,\n },\n {\n id: 'rev_3',\n platform: 'android',\n country: 'DE',\n rating: 4,\n title: null,\n body: 'Gute App, aber der Export dauert lange.',\n author: 'mgruber',\n submittedAt: '2026-09-03T16:45:00.000Z',\n replied: false,\n },\n ],\n};\n\n/** A 99.42% crash-free user rate with two issues, both on build 118. */\nexport const fixtureCrashes: TapCrashesResult = {\n crashFreeRate: 99.42,\n issues: [\n {\n id: 'crash_1',\n title: 'ExportSession.finish — index out of range',\n count: 214,\n build: '118',\n firstSeenAt: '2026-09-01T11:02:00.000Z',\n lastSeenAt: '2026-09-07T21:40:00.000Z',\n },\n {\n id: 'crash_2',\n title: 'PhotoLibrary.load — permission revoked',\n count: 37,\n build: '118',\n firstSeenAt: '2026-09-02T07:15:00.000Z',\n lastSeenAt: '2026-09-06T13:05:00.000Z',\n },\n ],\n};\n\n/** 41,200 USD of recurring revenue, 18 refunds, and a seven-day series under it. */\nexport const fixtureRevenue: TapRevenueResult = {\n currency: 'USD',\n mrr: 41200,\n refunds: 18,\n points: week().map((at, index) => ({\n at,\n value: [1310, 1355, 1401, 1388, 1470, 1525, 1502][index],\n })),\n};\n\n/** The shape of `fixtures`: every sandbox fixture under one name. */\nexport interface TapFixtures {\n project: TapProjectResult;\n apps: TapApp[];\n releases: TapRelease[];\n keywords: TapKeyword[];\n listings: TapListing[];\n reviews: TapReviewsResult;\n crashes: TapCrashesResult;\n revenue: TapRevenueResult;\n /** `fixtureSeries`, so a series is built per metric rather than held. */\n series(metric: TapSeriesMetric): TapSeriesResult;\n}\n\n/**\n * The sandbox data the mock bridge and the local preview both answer with, so a\n * component behaves the same in a test and in the preview.\n *\n * @remarks\n * These are the same objects the `fixture*` exports hold, gathered under one name.\n * `answerFixtureQuery` clones before it answers, but reading `fixtures` directly\n * hands you the live object, so a test that sorts or splices one has to copy it\n * first.\n *\n * @example\n * ```ts\n * import { fixtures } from '@tappify/extension-sdk/testing';\n *\n * const newest = [...fixtures.releases].sort((left, right) =>\n * right.version.localeCompare(left.version),\n * )[0];\n * ```\n */\nexport const fixtures: TapFixtures = {\n project: fixtureProject,\n apps: fixtureApps,\n releases: fixtureReleases,\n keywords: fixtureKeywords,\n listings: fixtureListings,\n reviews: fixtureReviews,\n crashes: fixtureCrashes,\n revenue: fixtureRevenue,\n series: fixtureSeries,\n};\n\n/**\n * Answers one `tap.data.query` from the fixtures, which is what the mock bridge\n * does.\n *\n * @remarks\n * Every branch is cloned with `structuredClone`, so a component that sorts or\n * splices a result cannot reach the fixture the next test reads. It checks no\n * scope — `createTapMock` refuses an ungranted kind before it gets here — and it\n * ignores the range and platform in the query, so the numbers are the same\n * whatever the filter bar says. The seven assertions inside are the one place\n * TypeScript cannot follow the distributive conditional in `TapResult<Q>` back\n * through a `switch`; each branch returns exactly the result its `case` selected.\n *\n * @example\n * ```ts\n * import { answerFixtureQuery } from '@tappify/extension-sdk/testing';\n *\n * const reviews = answerFixtureQuery({\n * kind: 'reviews',\n * range: { from: '2026-09-01', to: '2026-09-08' },\n * });\n * const lowest = Math.min(...reviews.reviews.map(review => review.rating));\n * ```\n */\nexport function answerFixtureQuery<Q extends TapQuery>(query: Q): TapResult<Q> {\n switch (query.kind) {\n case 'project':\n return structuredClone(fixtureProject) as TapResult<Q>;\n case 'series':\n return structuredClone(fixtureSeries(query.metric)) as TapResult<Q>;\n case 'keywords':\n return structuredClone({ keywords: fixtureKeywords }) as TapResult<Q>;\n case 'listing':\n return structuredClone({ listings: fixtureListings }) as TapResult<Q>;\n case 'reviews':\n return structuredClone(fixtureReviews) as TapResult<Q>;\n case 'crashes':\n return structuredClone(fixtureCrashes) as TapResult<Q>;\n case 'revenue':\n return structuredClone(fixtureRevenue) as TapResult<Q>;\n }\n}\n","import {\n exportJWK,\n generateKeyPair,\n SignJWT,\n type CryptoKey,\n type JSONWebKeySet,\n} from 'jose';\nimport { TapError } from '../client/errors';\nimport type { TapFilters } from '../client/types';\nimport { createTappifyHandler } from '../server/handler';\nimport type {\n ContextBlock,\n MentionItems,\n MetricsInput,\n MetricsResponse,\n TappifyClaims,\n TappifyHandlerOptions,\n TappifyHealth,\n TappifyInstall,\n} from '../server/types';\nimport type { WorkOperationMap } from '../server/work';\nimport {\n callEnvelope,\n DEFAULT_FILTERS,\n readEnvelopeResponse,\n} from './envelope';\n\nconst TEST_KID = 'tappify-test-key';\nconst TEST_ISSUER = 'https://api.tappify.ai';\n\ninterface TestKeys {\n privateKey: CryptoKey;\n jwks: JSONWebKeySet;\n}\n\nlet keys: Promise<TestKeys> | null = null;\n\nfunction ensureKeys(): Promise<TestKeys> {\n keys ??= (async () => {\n const pair = await generateKeyPair('RS256', { extractable: true });\n const publicJwk = await exportJWK(pair.publicKey);\n return {\n privateKey: pair.privateKey,\n jwks: {\n keys: [{ ...publicJwk, kid: TEST_KID, alg: 'RS256', use: 'sig' }],\n },\n };\n })();\n\n return keys;\n}\n\n/**\n * Returns the public key set that verifies the tokens `signTestToken` signs.\n *\n * @remarks\n * The pair is generated once per process and cached, so every call in a suite\n * returns the same set. Pass it as `jwks` to `createTappifyHandler` when a test\n * drives the handler itself; `createTestClient` does that for you.\n *\n * @example\n * ```ts\n * import { createTappifyHandler } from '@tappify/extension-sdk/server';\n * import { testJwks } from '@tappify/extension-sdk/testing';\n *\n * async function buildHandler() {\n * return createTappifyHandler({ extensionId: 'starter', jwks: await testJwks() });\n * }\n * ```\n */\nexport async function testJwks(): Promise<JSONWebKeySet> {\n return (await ensureKeys()).jwks;\n}\n\n/**\n * Signs an install token a handler built against `testJwks` accepts.\n *\n * @remarks\n * Every claim has a default — install `ins_test`, extension `starter`, project\n * `prj_north`, organization `org_test`, no scopes — and the audience is\n * `ext:<extensionId>`, taken from `options.extensionId`, then from the claims, then\n * `starter`. It expires in five minutes unless `expiresIn` says otherwise, which is\n * how a test covers an expired token. `createTestClient` and `createTapMock` sign\n * their own tokens, so reach for this only when a test verifies a token itself.\n *\n * @example\n * ```ts\n * import { verifyTappifyToken } from '@tappify/extension-sdk/server';\n * import { signTestToken, testJwks } from '@tappify/extension-sdk/testing';\n *\n * async function claimsOfTestToken() {\n * const token = await signTestToken({ scopes: ['analytics:read'] });\n * return verifyTappifyToken(token, {\n * extensionId: 'starter',\n * jwks: await testJwks(),\n * });\n * }\n * ```\n */\nexport async function signTestToken(\n claims: Partial<TappifyClaims> = {},\n options: { extensionId?: string; expiresIn?: string } = {},\n): Promise<string> {\n const { privateKey } = await ensureKeys();\n const extensionId = options.extensionId ?? claims.extensionId ?? 'starter';\n\n return new SignJWT({\n installId: claims.installId ?? 'ins_test',\n extensionId,\n projectId: claims.projectId ?? 'prj_north',\n organizationId: claims.organizationId ?? 'org_test',\n scopes: claims.scopes ?? [],\n ...(claims.userId === undefined ? {} : { userId: claims.userId }),\n })\n .setProtectedHeader({ alg: 'RS256', kid: TEST_KID })\n .setIssuer(TEST_ISSUER)\n .setAudience(`ext:${extensionId}`)\n .setIssuedAt()\n .setExpirationTime(options.expiresIn ?? '5m')\n .sign(privateKey);\n}\n\n/** What every call a `TestClient` makes carries, unless a call overrides it. */\nexport interface TestClientDefaults {\n /** Merged over the default install, whose id is `ins_test`. */\n install?: Partial<TappifyInstall>;\n /** The scopes the token claims. Defaults to none. */\n scopes?: string[];\n /** What the handler reads as `credentials`. Defaults to empty. */\n credentials?: Record<string, unknown>;\n /** What the handler reads as `documents`, including `settings`. */\n documents?: Record<string, unknown>;\n /** The filter bar behind the call. Defaults to a seven-day September range. */\n filters?: TapFilters;\n /** One fixed idempotency key. Left out, each call gets its own. */\n eventId?: string;\n /** The origin the request url is built on. Defaults to `https://vendor.test`. */\n origin?: string;\n}\n\n/** One method per route Tappify calls, plus `raw` for the response itself. */\nexport interface TestClient {\n /** Calls the health route, which carries no token. */\n health(): Promise<TappifyHealth>;\n /** Calls the connector's metrics route. */\n metrics(input: MetricsInput): Promise<MetricsResponse>;\n /** Calls one tool by id and returns the card it answered with. */\n tool(id: string, input: unknown): Promise<unknown>;\n /** Calls one mention by id, with the query as the `q` parameter. */\n mention(id: string, query: string): Promise<MentionItems>;\n /** Calls one action by id. */\n action(id: string, input: unknown): Promise<unknown>;\n /** Calls one procedure by name; the input defaults to an empty object. */\n procedure(name: string, input?: unknown): Promise<unknown>;\n /** Calls one context block by id. */\n context(id: string): Promise<ContextBlock>;\n /** Posts one Tappify event, which the handler answers with 204. */\n event(name: string, payload: Record<string, unknown>): Promise<void>;\n /** Calls one work operation. */\n work<O extends keyof WorkOperationMap>(\n operation: O,\n input: WorkOperationMap[O]['input'],\n ): Promise<WorkOperationMap[O]['output']>;\n /** @deprecated Call set_status or describe_container using the current work protocol. */\n work(operation: 'status' | 'resolve', input: unknown): Promise<unknown>;\n /**\n * Sends any method and path and returns the `Response`, for a test that asserts\n * on a status rather than a body.\n */\n raw(method: string, path: string, body?: unknown): Promise<Response>;\n}\n\nconst HEALTH_PATH = '/tappify/health';\n\n/**\n * Drives a vendor server the way Tappify does, so a route is tested without a\n * running Tappify.\n *\n * @remarks\n * Pass the options object you hand `createTappifyHandler`, not the handler it\n * returns: the client rebuilds the handler around `testJwks` and signs each call\n * with a matching token. A built handler throws a `TapError` carrying\n * `TAP_TEST_TARGET_INVALID`, because it would verify against the live Tappify key\n * set and answer 401. Every call carries a fresh event id and the envelope Tappify\n * sends, and every call but `health` a signed token, so the handler reads the same\n * `install`, `context`, `credentials` and `documents` it will read in production. A route that\n * answers an error rejects with a `TapServerError` carrying the code, message and\n * status from the body, which is what a test asserts on; use `raw` when the status\n * itself is the assertion. Every method calls the unprefixed path — `/tappify/…` —\n * whatever `basePath` the options carry, so a `basePath` the handler strips off\n * something else is not exercised here, and one that is itself a prefix of\n * `/tappify`, such as `/tap`, makes every call 404 `TAP_ROUTE_UNKNOWN`. Reach a\n * prefixed path through `raw` when that is what you need to cover.\n *\n * @example\n * ```ts\n * import type { TappifyHandlerOptions } from '@tappify/extension-sdk/server';\n * import { createTestClient } from '@tappify/extension-sdk/testing';\n * import { expect, it } from 'vitest';\n *\n * const handlerOptions: TappifyHandlerOptions = {\n * extensionId: 'starter',\n * health: () => ({ ok: true, version: '0.1.0' }),\n * };\n * const client = createTestClient(handlerOptions, { scopes: ['analytics:read'] });\n *\n * it('answers the health check', async () => {\n * await expect(client.health()).resolves.toEqual({ ok: true, version: '0.1.0' });\n * });\n * ```\n */\nexport function createTestClient(\n target: TappifyHandlerOptions,\n defaults: TestClientDefaults = {},\n): TestClient {\n if (typeof target === 'function') {\n throw new TapError(\n 'TAP_TEST_TARGET_INVALID',\n 'createTestClient() was handed a built handler instead of the options object. A handler from createTappifyHandler() verifies install tokens against the live Tappify key set, so every call the test client makes would come back 401. Pass the options themselves — createTestClient(handlerOptions) — or, when you need the handler, build it with createTappifyHandler({ ...handlerOptions, jwks: await testJwks() }) and drive it through createTapMock({ handler, extensionId: handlerOptions.extensionId }) so the test token carries the audience the handler expects.',\n );\n }\n\n const extensionId = target.extensionId;\n const handlerPromise = testJwks().then(jwks =>\n createTappifyHandler({ ...target, jwks }),\n );\n\n const install: TappifyInstall = {\n id: 'ins_test',\n extensionId,\n projectId: 'prj_north',\n organizationId: 'org_test',\n ...defaults.install,\n };\n\n const filters = defaults.filters ?? DEFAULT_FILTERS;\n const origin = defaults.origin ?? 'https://vendor.test';\n\n let sent = 0;\n\n function envelope(input: unknown, event?: unknown): string {\n return callEnvelope({\n install,\n filters,\n input,\n credentials: defaults.credentials,\n documents: defaults.documents,\n event,\n });\n }\n\n async function send(\n method: string,\n path: string,\n body?: string,\n ): Promise<Response> {\n const handler = await handlerPromise;\n sent += 1;\n const headers = new Headers({\n 'content-type': 'application/json',\n 'x-tappify-event-id': defaults.eventId ?? `evt_${path}_${String(sent)}`,\n });\n\n if (path !== HEALTH_PATH) {\n const token = await signTestToken(\n {\n installId: install.id,\n extensionId: install.extensionId,\n projectId: install.projectId,\n organizationId: install.organizationId,\n scopes: defaults.scopes ?? [],\n },\n { extensionId },\n );\n headers.set('authorization', `Bearer ${token}`);\n }\n\n return handler(\n new Request(`${origin}${path}`, {\n method,\n headers,\n ...(body === undefined ? {} : { body }),\n }),\n );\n }\n\n async function call(\n method: string,\n path: string,\n body?: string,\n ): Promise<unknown> {\n return readEnvelopeResponse(\n await send(method, path, body),\n `${method} ${path}`,\n );\n }\n\n return {\n health: async () => (await call('GET', HEALTH_PATH)) as TappifyHealth,\n metrics: async input =>\n (await call(\n 'POST',\n '/tappify/metrics',\n envelope(input),\n )) as MetricsResponse,\n tool: (id, input) => call('POST', `/tappify/tools/${id}`, envelope(input)),\n mention: async (id, query) =>\n (await call(\n 'GET',\n `/tappify/mentions/${id}?q=${encodeURIComponent(query)}`,\n )) as MentionItems,\n action: (id, input) =>\n call('POST', `/tappify/actions/${id}`, envelope(input)),\n procedure: (name, input = {}) =>\n call('POST', `/tappify/procedures/${name}`, envelope(input)),\n context: async id =>\n (await call('GET', `/tappify/context/${id}`)) as ContextBlock,\n event: async (name, payload) => {\n await call('POST', '/tappify/events', envelope({}, { name, payload }));\n },\n work: (async (operation: string, input: unknown) =>\n call(\n 'POST',\n `/tappify/work/${operation}`,\n envelope(input),\n )) as TestClient['work'],\n raw: (method, path, body) =>\n send(method, path, body === undefined ? undefined : JSON.stringify(body)),\n };\n}\n","import { TapServerError } from '../client/errors';\nimport type { TapFilters } from '../client/types';\nimport { SERVER_ERROR_CODES } from '../server/codes';\nimport type { TappifyCallContext, TappifyInstall } from '../server/types';\nimport { readRecord, readText } from '../server/wire';\n\nexport const DEFAULT_FILTERS: TapFilters = {\n range: { from: '2026-09-01', to: '2026-09-08', preset: '7d' },\n platform: 'all',\n country: 'all',\n};\n\nexport interface CallEnvelope {\n install: TappifyInstall;\n filters: TapFilters;\n input: unknown;\n credentials?: Record<string, unknown>;\n documents?: Record<string, unknown>;\n event?: unknown;\n}\n\nexport function callEnvelope(envelope: CallEnvelope): string {\n const context: TappifyCallContext = {\n projectId: envelope.install.projectId,\n platform: envelope.filters.platform,\n from: envelope.filters.range.from,\n to: envelope.filters.range.to,\n filters: envelope.filters,\n };\n\n return JSON.stringify({\n install: envelope.install,\n context,\n input: envelope.input,\n credentials: envelope.credentials ?? {},\n documents: envelope.documents ?? {},\n event: envelope.event,\n });\n}\n\nfunction parseJson(text: string): { parsed: boolean; value: unknown } {\n try {\n return { parsed: true, value: JSON.parse(text) as unknown };\n } catch {\n return { parsed: false, value: undefined };\n }\n}\n\nexport async function readEnvelopeResponse(\n response: Response,\n call: string,\n): Promise<unknown> {\n const text = await response.text();\n const body = parseJson(text);\n const status = String(response.status);\n\n if (response.ok) {\n if (body.parsed) return body.value;\n if (text.trim() === '') return undefined;\n\n throw new TapServerError(\n SERVER_ERROR_CODES.INTERNAL_ERROR,\n `The extension server answered ${status} for ${call} with a body that is not JSON. Every Tappify route answers JSON, or 204 with no body.`,\n response.status,\n );\n }\n\n const error = readRecord(body.value, 'error');\n\n throw new TapServerError(\n readText(error, 'code') ?? SERVER_ERROR_CODES.INTERNAL_ERROR,\n readText(error, 'message') ??\n `The extension server answered ${status} for ${call} without a JSON error body.`,\n response.status,\n );\n}\n","import { TapError } from '../client/errors';\nimport type {\n Tap,\n TapChangeKey,\n TapChatCard,\n TapCollection,\n TapConfirmOptions,\n TapFilters,\n TapHostEvents,\n TapProcedures,\n TapSingleton,\n TapSize,\n TapStorage,\n TapToastTone,\n} from '../client/types';\nimport { QUERY_SCOPES, SCOPE_KEYS, type ScopeKey } from '../manifest/constants';\nimport type { TappifyFetchHandler } from '../server/handler';\nimport { signTestToken } from './client';\nimport {\n callEnvelope,\n DEFAULT_FILTERS,\n readEnvelopeResponse,\n} from './envelope';\nimport { answerFixtureQuery } from './fixtures';\n\n/**\n * Everything the component asked the host to do, in call order, for a test to\n * assert on.\n */\nexport interface TapMockCalls {\n /** One entry per `tap.nav.push`. */\n navigations: string[];\n /** One entry per `tap.nav.setSearch`. */\n searches: Record<string, string | null>[];\n /** One entry per `tap.ui.toast`; the tone defaults to `neutral`. */\n toasts: { message: string; tone: TapToastTone }[];\n /** One entry per `tap.ui.confirm`, whatever the mock answered. */\n confirms: TapConfirmOptions[];\n /** One entry per `tap.actions.run`, which the mock records rather than refuses. */\n actions: { actionId: string; input: unknown }[];\n /** One entry per `tap.telemetry.event`. */\n telemetry: {\n name: string;\n props?: Record<string, string | number | boolean>;\n }[];\n /** One entry per `tap.nav.openChat`. */\n chats: { prompt: string; context?: Record<string, unknown> }[];\n /** One url per `tap.nav.openExternal`, with no confirmation asked. */\n externals: string[];\n /** The filename and byte size per `tap.ui.download`, not the blob itself. */\n downloads: { filename: string; size: number }[];\n /** One entry per `tap.ui.copy`, with no clipboard touched. */\n copies: string[];\n /** One card per `tap.ui.openInChat`, unformatted. */\n cards: TapChatCard[];\n /** How many times `tap.ui.expand` was called. */\n expands: number;\n /** How many times `tap.nav.openSettings` was called. */\n settingsOpens: number;\n}\n\n/**\n * How the mock bridge is built, and the options `renderWithTap` forwards to it.\n */\nexport interface CreateTapMockOptions {\n /** Defaults to `ins_test`. */\n installId?: string;\n /** Defaults to `starter`, and has to match `handler`'s own extension id. */\n extensionId?: string;\n /** Defaults to `Starter`. */\n extensionName?: string;\n /** What the install granted. Defaults to every scope. */\n scopes?: ScopeKey[];\n /** What `tap.ui.size` reports. Defaults to `slot`. */\n size?: TapSize;\n /** Merged over the fixture filter bar. */\n filters?: Partial<TapFilters>;\n /** Merged over the fixture Northlight project. */\n project?: Partial<Tap['project']>;\n /** What `tap.context` holds. Defaults to empty. */\n context?: Record<string, unknown>;\n /** What `tap.params` holds, in the host's positional shape. Defaults to empty. */\n params?: Record<string, string>;\n /** What `useTapTheme` reports. Defaults to `light`. */\n theme?: 'light' | 'dark';\n /** What `tap.ui.confirm` resolves to. Defaults to `true`. */\n confirmAnswer?: boolean;\n /** The collections to expose, and the kind of each. Left out, every name works. */\n storage?: Record<string, 'singleton' | 'collection'>;\n /** One function per procedure, answering `tap.server.<name>` in process. */\n server?: Record<string, (input: unknown) => unknown>;\n /** Your own handler, so `tap.server.<name>` goes through its real routes. */\n handler?: TappifyFetchHandler;\n /** A token to send to `handler`. Defaults to one the mock signs itself. */\n handlerToken?: string;\n}\n\n/** The mock bridge, with the recorded calls and the handles that drive it. */\nexport interface TapMock {\n /** The bridge itself, which `renderWithTap` puts above the component. */\n tap: Tap;\n calls: TapMockCalls;\n /** The node SDK dialogs render into, appended to the document by `renderWithTap`. */\n portal: HTMLElement;\n /** What a collection holds now, or just the one document under `documentId`. */\n documents(collection: string, documentId?: string): unknown[];\n /** Delivers a Tappify event to everything subscribed through `tap.data.subscribe`. */\n emit<E extends keyof TapHostEvents>(\n event: E,\n payload: TapHostEvents[E],\n ): void;\n /** Replaces the whole filter bar and re-renders the hooks that read it. */\n setFilters(filters: TapFilters): void;\n /** Moves the mount between slot, panel and page. */\n setSize(size: TapSize): void;\n /** Switches the theme `useTapTheme` reports. */\n setTheme(mode: 'light' | 'dark'): void;\n /** Replaces the route `useTapParams` reports, in the host's own positional shape. */\n setParams(params: Record<string, string>): void;\n}\n\ninterface StoredDocument {\n id: string;\n data: unknown;\n}\n\ntype StorageHandle = TapSingleton<unknown> & TapCollection<unknown>;\n\ntype ExposedHandle = TapSingleton<unknown> | TapCollection<unknown>;\n\nconst SINGLETON_ID = 'singleton';\n\nconst PROBE_KEYS = new Set([\n 'then',\n 'toJSON',\n '$$typeof',\n 'constructor',\n 'prototype',\n 'valueOf',\n 'toString',\n 'inspect',\n 'asymmetricMatch',\n 'nodeType',\n]);\n\nfunction unknownCollectionError(collection: string): TapError {\n return new TapError(\n 'TAP_UNKNOWN_COLLECTION',\n `This mock exposes no storage collection called \"${collection}\". createTapMock({ storage: … }) was given an explicit map, so only the collections it names exist. Add \"${collection}\" to that map, or drop the storage option to expose every collection.`,\n );\n}\n\nfunction singletonView(handle: StorageHandle): TapSingleton<unknown> {\n return {\n get: () => handle.get(),\n set: document => handle.set(document),\n patch: partial => handle.patch(partial),\n };\n}\n\nfunction collectionView(handle: StorageHandle): TapCollection<unknown> {\n return {\n list: listOptions => handle.list(listOptions),\n get: id => handle.get(id),\n put: (id, document) => handle.put(id, document),\n delete: id => handle.delete(id),\n };\n}\n\nfunction scopeError(scope: ScopeKey): TapError {\n return new TapError(\n 'TAP_SCOPE_MISSING',\n `This install has not granted ${scope}. Declare it under \"scopes\" in tappify.extension.json, or pass scopes: [\"${scope}\"] to renderWithTap() to test the granted path.`,\n );\n}\n\nfunction asObject(value: unknown): object {\n return typeof value === 'object' && value !== null ? value : {};\n}\n\n/**\n * Builds the bridge a component talks to in a test, without rendering anything.\n *\n * @remarks\n * Data queries answer from the sandbox fixtures, storage is an in-memory store, and\n * everything else the host would do is recorded in `calls`. Scopes default to all of\n * them: pass `scopes` to test the refused path, where a data query rejects with\n * `TAP_SCOPE_MISSING` for the kind's own scope and every storage call, reads\n * included, needs `storage:write`. A procedure needs either `server`, which answers\n * in process, or `handler`, which is called over a real `Request` with a token the\n * mock signs — so that handler has to be built with `jwks: await testJwks()` and the\n * same `extensionId` — and without either, `tap.server.<name>` rejects with\n * `TAP_UNKNOWN_PROCEDURE`. With `storage` given, a collection the map does not name\n * throws `TAP_UNKNOWN_COLLECTION`. Unlike the host, this mock records\n * `tap.actions.run` and resolves it as pending rather than refusing it, so a test\n * can assert on the call today. Use `renderWithTap` when there is a component to\n * render.\n *\n * @example\n * ```ts\n * import { createTapMock } from '@tappify/extension-sdk/testing';\n * import { expect, it } from 'vitest';\n *\n * it('answers a keyword query from the fixtures', async () => {\n * const mock = createTapMock({ scopes: ['store.metadata:read'] });\n * const keywords = await mock.tap.data.query({ kind: 'keywords' });\n * expect(keywords.keywords).toHaveLength(3);\n * });\n * ```\n */\nexport function createTapMock(options: CreateTapMockOptions = {}): TapMock {\n const granted = new Set<ScopeKey>(options.scopes ?? SCOPE_KEYS);\n const listeners = new Map<TapChangeKey, Set<() => void>>();\n const eventHandlers = new Map<string, Set<(payload: unknown) => void>>();\n const state = new Map<string, unknown>();\n const store = new Map<string, StoredDocument[]>();\n\n const portal = document.createElement('div');\n portal.dataset.tapPortal = 'true';\n\n const calls: TapMockCalls = {\n navigations: [],\n searches: [],\n toasts: [],\n confirms: [],\n actions: [],\n telemetry: [],\n chats: [],\n externals: [],\n downloads: [],\n copies: [],\n cards: [],\n expands: 0,\n settingsOpens: 0,\n };\n\n const emitChange = (key: TapChangeKey): void => {\n for (const listener of listeners.get(key) ?? []) listener();\n };\n\n const subscribe = (key: TapChangeKey, listener: () => void): (() => void) => {\n const set = listeners.get(key) ?? new Set<() => void>();\n set.add(listener);\n listeners.set(key, set);\n return () => {\n set.delete(listener);\n };\n };\n\n const withStorage = <T>(run: () => T): Promise<T> =>\n granted.has('storage:write')\n ? Promise.resolve(run())\n : Promise.reject(scopeError('storage:write'));\n\n const bucket = (collection: string): StoredDocument[] => {\n const existing = store.get(collection);\n if (existing) return existing;\n const created: StoredDocument[] = [];\n store.set(collection, created);\n return created;\n };\n\n function buildHandle(collection: string): StorageHandle {\n const find = (id: string): StoredDocument | undefined =>\n bucket(collection).find(entry => entry.id === id);\n\n const write = (id: string, data: unknown): void => {\n const items = bucket(collection);\n const index = items.findIndex(entry => entry.id === id);\n if (index === -1) items.push({ id, data });\n else items[index] = { id, data };\n emitChange(`storage:${collection}`);\n };\n\n return {\n get: (id?: string) =>\n withStorage(() => find(id ?? SINGLETON_ID)?.data ?? null),\n set: document =>\n withStorage(() => {\n write(SINGLETON_ID, document);\n }),\n patch: partial =>\n withStorage(() => {\n const merged = { ...asObject(find(SINGLETON_ID)?.data), ...partial };\n write(SINGLETON_ID, merged);\n return merged;\n }),\n list: () =>\n withStorage(() => ({\n items: bucket(collection)\n .filter(entry => entry.id !== SINGLETON_ID)\n .map(entry => ({ ...asObject(entry.data), id: entry.id })),\n })),\n put: (id, document) =>\n withStorage(() => {\n write(id, document);\n }),\n delete: id =>\n withStorage(() => {\n const items = bucket(collection);\n const index = items.findIndex(entry => entry.id === id);\n if (index !== -1) {\n items.splice(index, 1);\n emitChange(`storage:${collection}`);\n }\n }),\n };\n }\n\n const handles = new Map<string, ExposedHandle>();\n const kinds = options.storage;\n\n const exposeHandle = (collection: string): ExposedHandle => {\n const kind = kinds?.[collection];\n if (kinds !== undefined && kind === undefined) {\n throw unknownCollectionError(collection);\n }\n\n const handle = buildHandle(collection);\n if (kind === 'singleton') return singletonView(handle);\n if (kind === 'collection') return collectionView(handle);\n return handle;\n };\n\n const handleFor = (collection: string): ExposedHandle | undefined => {\n if (PROBE_KEYS.has(collection)) return undefined;\n\n const existing = handles.get(collection);\n if (existing) return existing;\n const created = exposeHandle(collection);\n handles.set(collection, created);\n return created;\n };\n\n const storageTarget: TapStorage = {};\n const storage = new Proxy(storageTarget, {\n get: (_target, property) =>\n typeof property === 'string' ? handleFor(property) : undefined,\n has: () => true,\n ownKeys: () => [...store.keys()],\n getOwnPropertyDescriptor: () => ({ enumerable: true, configurable: true }),\n });\n\n let calledProcedures = 0;\n let signedToken: Promise<string> | null = null;\n\n function handlerToken(): Promise<string> {\n if (options.handlerToken !== undefined) {\n return Promise.resolve(options.handlerToken);\n }\n signedToken ??= signTestToken(\n {\n installId: tap.extension.installId,\n extensionId: tap.extension.id,\n projectId: tap.extension.projectId,\n organizationId: tap.auth.organizationId,\n scopes: [...granted],\n },\n { extensionId: tap.extension.id },\n );\n return signedToken;\n }\n\n async function callProcedure(name: string, input: unknown): Promise<unknown> {\n const local = options.server?.[name];\n if (local) return local(input);\n\n if (options.handler) {\n const path = `/tappify/procedures/${name}`;\n calledProcedures += 1;\n const response = await options.handler(\n new Request(`https://vendor.test${path}`, {\n method: 'POST',\n headers: {\n 'content-type': 'application/json',\n 'x-tappify-event-id': `evt_${name}_${String(calledProcedures)}`,\n authorization: `Bearer ${await handlerToken()}`,\n },\n body: callEnvelope({\n install: {\n id: tap.extension.installId,\n extensionId: tap.extension.id,\n projectId: tap.extension.projectId,\n organizationId: tap.auth.organizationId,\n },\n filters: tap.filters,\n input,\n }),\n }),\n );\n\n return readEnvelopeResponse(response, `POST ${path}`);\n }\n\n throw new TapError(\n 'TAP_UNKNOWN_PROCEDURE',\n `No handler is registered for the procedure \"${name}\". Pass server: { ${name}: input => … } or handler: <your createTappifyHandler handler> to createTapMock().`,\n );\n }\n\n const serverTarget: TapProcedures = {};\n\n const tap: Tap = {\n extension: {\n id: options.extensionId ?? 'starter',\n name: options.extensionName ?? 'Starter',\n installId: options.installId ?? 'ins_test',\n projectId: 'prj_north',\n },\n env: 'dev',\n host: { version: 'test', sdkMajors: [2] },\n auth: {\n token: 'test-token',\n user: { id: 'usr_test', role: 'admin' },\n organizationId: 'org_test',\n scopes: [...granted],\n can: scope => granted.has(scope),\n },\n project: {\n id: 'prj_north',\n name: 'Northlight',\n platforms: ['ios', 'android'],\n apps: [],\n releases: [],\n keywords: [],\n ...options.project,\n },\n filters: { ...DEFAULT_FILTERS, ...options.filters },\n theme: { mode: options.theme ?? 'light' },\n locale: 'en-US',\n timezone: 'UTC',\n format: {\n number: (value, formatOptions) =>\n new Intl.NumberFormat('en-US', formatOptions).format(value),\n currency: (value, code) =>\n new Intl.NumberFormat('en-US', {\n style: 'currency',\n currency: code,\n }).format(value),\n date: value => new Date(value).toISOString().slice(0, 10),\n relative: value => new Date(value).toISOString(),\n },\n nav: {\n push: path => calls.navigations.push(path),\n setSearch: params => calls.searches.push(params),\n openSettings: () => {\n calls.settingsOpens += 1;\n },\n openChat: (prompt, context) => calls.chats.push({ prompt, context }),\n openExternal: url => calls.externals.push(url),\n },\n data: {\n query: query => {\n const scope = QUERY_SCOPES[query.kind];\n if (!granted.has(scope)) return Promise.reject(scopeError(scope));\n return Promise.resolve(answerFixtureQuery(query));\n },\n subscribe: <E extends keyof TapHostEvents>(\n event: E,\n handler: (payload: TapHostEvents[E]) => void,\n ): (() => void) => {\n const key = String(event);\n const set = eventHandlers.get(key) ?? new Set<(p: unknown) => void>();\n const wrapped = (payload: unknown): void => {\n handler(payload as TapHostEvents[E]);\n };\n set.add(wrapped);\n eventHandlers.set(key, set);\n return () => {\n set.delete(wrapped);\n };\n },\n },\n server: new Proxy(serverTarget, {\n get:\n (_target, property) =>\n (input: unknown): Promise<unknown> =>\n callProcedure(String(property), input),\n has: () => true,\n }),\n state: {\n get: <T>(key: string): T | undefined => {\n const value = state.get(key);\n return value === undefined ? undefined : (value as T);\n },\n set: (key, value) => {\n state.set(key, value);\n emitChange(`state:${key}`);\n },\n subscribe: (key, handler) => subscribe(`state:${key}`, handler),\n },\n ui: {\n size: options.size ?? 'slot',\n toast: (message, tone = 'neutral') =>\n calls.toasts.push({ message, tone }),\n confirm: confirmOptions => {\n calls.confirms.push(confirmOptions);\n return Promise.resolve(options.confirmAnswer ?? true);\n },\n openInChat: card => calls.cards.push(card),\n expand: () => {\n calls.expands += 1;\n },\n download: (blob, filename) =>\n calls.downloads.push({ filename, size: blob.size }),\n copy: text => {\n calls.copies.push(text);\n return Promise.resolve();\n },\n },\n context: options.context ?? {},\n params: options.params ?? {},\n storage,\n telemetry: {\n event: (name, props) => calls.telemetry.push({ name, props }),\n },\n actions: {\n run: (actionId, input) => {\n calls.actions.push({ actionId, input });\n return Promise.resolve({\n id: `run_${String(calls.actions.length)}`,\n actionId,\n status: 'pending' as const,\n requestedAt: new Date(0).toISOString(),\n });\n },\n },\n invalidate: () => undefined,\n __portal: portal,\n __subscribe: subscribe,\n };\n\n return {\n tap,\n calls,\n portal,\n documents: (collection, documentId) =>\n bucket(collection)\n .filter(entry => documentId === undefined || entry.id === documentId)\n .map(entry => entry.data),\n emit: (event, payload) => {\n for (const handler of eventHandlers.get(String(event)) ?? []) {\n handler(payload);\n }\n },\n setFilters: filters => {\n tap.filters = filters;\n emitChange('filters');\n },\n setSize: size => {\n tap.ui = { ...tap.ui, size };\n emitChange('size');\n },\n setTheme: mode => {\n tap.theme = { mode };\n emitChange('theme');\n },\n setParams: params => {\n tap.params = params;\n emitChange('params');\n },\n };\n}\n"]}
1
+ {"version":3,"sources":["/home/runner/work/extension-sdk/extension-sdk/dist/chunk-AYYJCRS4.cjs","../src/testing/fixtures.ts","../src/testing/client.ts","../src/testing/envelope.ts","../src/testing/mock.ts"],"names":[],"mappings":"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","file":"/home/runner/work/extension-sdk/extension-sdk/dist/chunk-AYYJCRS4.cjs","sourcesContent":[null,"import type {\n TapApp,\n TapCrashesResult,\n TapKeyword,\n TapListing,\n TapProjectResult,\n TapQuery,\n TapRelease,\n TapResult,\n TapRevenueResult,\n TapReviewsResult,\n TapSeriesMetric,\n TapSeriesPoint,\n TapSeriesResult,\n} from '../client/types';\n\n/** The owner's two Northlight apps, one per platform. */\nexport const fixtureApps: TapApp[] = [\n {\n id: 'app_ios',\n name: 'Northlight',\n platform: 'ios',\n storeId: '1490000001',\n bundleId: 'ai.tappify.northlight',\n },\n {\n id: 'app_android',\n name: 'Northlight',\n platform: 'android',\n storeId: 'ai.tappify.northlight',\n bundleId: 'ai.tappify.northlight',\n },\n];\n\n/** Three shipped releases: two iOS versions and the Android build of the newer one. */\nexport const fixtureReleases: TapRelease[] = [\n {\n id: 'rel_42',\n version: '4.2.0',\n build: '118',\n platform: 'ios',\n state: 'ready_for_sale',\n shippedAt: '2026-09-01T09:00:00.000Z',\n },\n {\n id: 'rel_41',\n version: '4.1.0',\n build: '107',\n platform: 'ios',\n state: 'ready_for_sale',\n shippedAt: '2026-08-11T09:00:00.000Z',\n },\n {\n id: 'rel_a42',\n version: '4.2.0',\n build: '4200',\n platform: 'android',\n state: 'published',\n shippedAt: '2026-09-02T09:00:00.000Z',\n },\n];\n\n/**\n * Three tracked terms with positions, two on iOS and one on Android, scored on\n * the host's 1 to 10 traffic scale.\n */\nexport const fixtureKeywords: TapKeyword[] = [\n {\n id: 'kw_1',\n term: 'photo editor',\n platform: 'ios',\n country: 'US',\n position: 4,\n popularity: 6.6,\n },\n {\n id: 'kw_2',\n term: 'collage maker',\n platform: 'ios',\n country: 'US',\n position: 11,\n popularity: 5.3,\n },\n {\n id: 'kw_3',\n term: 'photo editor',\n platform: 'android',\n country: 'US',\n position: 7,\n popularity: 6,\n },\n];\n\n/** One store listing, for iOS in `en-US`. */\nexport const fixtureListings: TapListing[] = [\n {\n platform: 'ios',\n country: 'US',\n locale: 'en-US',\n title: 'Northlight — Photo Editor',\n subtitle: 'Edit, collage, share',\n description: 'Northlight turns a camera roll into a finished post.',\n keywords: ['photo', 'editor', 'collage'],\n updatedAt: '2026-08-28T12:00:00.000Z',\n },\n];\n\n/** The Northlight project, with `fixtureApps` and `fixtureReleases` under it. */\nexport const fixtureProject: TapProjectResult = {\n project: {\n id: 'prj_north',\n name: 'Northlight',\n platforms: ['ios', 'android'],\n },\n apps: fixtureApps,\n releases: fixtureReleases,\n};\n\nconst SERIES_VALUES: Record<TapSeriesMetric, number[]> = {\n downloads: [1180, 1240, 1310, 1290, 1402, 1512, 1488],\n impressions: [21400, 22150, 22980, 22110, 24020, 25510, 25010],\n page_views: [8120, 8340, 8710, 8290, 9110, 9640, 9420],\n conversion_rate: [0.145, 0.149, 0.15, 0.156, 0.152, 0.157, 0.158],\n};\n\nfunction week(): string[] {\n return [\n '2026-09-01',\n '2026-09-02',\n '2026-09-03',\n '2026-09-04',\n '2026-09-05',\n '2026-09-06',\n '2026-09-07',\n ];\n}\n\n/**\n * Builds the seven-day series for one metric, over the first week of September\n * 2026.\n *\n * @remarks\n * The same seven values every call, so an assertion on a total or a peak is\n * stable: downloads peak at 1,512 and `conversion_rate` comes back with unit\n * `ratio` where the other three come back as `count`. The range in the query does\n * not change the points, so a test that drives the filter bar asserts on the call,\n * not on different numbers.\n *\n * @example\n * ```ts\n * import { fixtureSeries } from '@tappify/extension-sdk/testing';\n *\n * const downloads = fixtureSeries('downloads');\n * const best = Math.max(...downloads.points.map(point => point.value));\n * ```\n */\nexport function fixtureSeries(metric: TapSeriesMetric): TapSeriesResult {\n const points: TapSeriesPoint[] = week().map((at, index) => ({\n at,\n value: SERIES_VALUES[metric][index],\n }));\n\n return {\n metric,\n unit: metric === 'conversion_rate' ? 'ratio' : 'count',\n points,\n };\n}\n\n/** Three reviews across three countries, averaging 3.7 stars, one already replied to. */\nexport const fixtureReviews: TapReviewsResult = {\n total: 3,\n averageRating: 3.7,\n reviews: [\n {\n id: 'rev_1',\n platform: 'ios',\n country: 'US',\n rating: 5,\n title: 'Fast and clean',\n body: 'The new collage layouts are exactly what I wanted.',\n author: 'kbrady',\n submittedAt: '2026-09-05T18:22:00.000Z',\n replied: false,\n },\n {\n id: 'rev_2',\n platform: 'ios',\n country: 'GB',\n rating: 2,\n title: 'Crashes on export',\n body: 'Since 4.2 the export button closes the app.',\n author: null,\n submittedAt: '2026-09-04T08:10:00.000Z',\n replied: true,\n },\n {\n id: 'rev_3',\n platform: 'android',\n country: 'DE',\n rating: 4,\n title: null,\n body: 'Gute App, aber der Export dauert lange.',\n author: 'mgruber',\n submittedAt: '2026-09-03T16:45:00.000Z',\n replied: false,\n },\n ],\n};\n\n/** A 99.42% crash-free user rate with two issues, both on build 118. */\nexport const fixtureCrashes: TapCrashesResult = {\n crashFreeRate: 99.42,\n issues: [\n {\n id: 'crash_1',\n title: 'ExportSession.finish — index out of range',\n count: 214,\n build: '118',\n firstSeenAt: '2026-09-01T11:02:00.000Z',\n lastSeenAt: '2026-09-07T21:40:00.000Z',\n },\n {\n id: 'crash_2',\n title: 'PhotoLibrary.load — permission revoked',\n count: 37,\n build: '118',\n firstSeenAt: '2026-09-02T07:15:00.000Z',\n lastSeenAt: '2026-09-06T13:05:00.000Z',\n },\n ],\n};\n\n/** 41,200 USD of recurring revenue, 18 refunds, and a seven-day series under it. */\nexport const fixtureRevenue: TapRevenueResult = {\n currency: 'USD',\n mrr: 41200,\n refunds: 18,\n points: week().map((at, index) => ({\n at,\n value: [1310, 1355, 1401, 1388, 1470, 1525, 1502][index],\n })),\n};\n\n/** The shape of `fixtures`: every sandbox fixture under one name. */\nexport interface TapFixtures {\n project: TapProjectResult;\n apps: TapApp[];\n releases: TapRelease[];\n keywords: TapKeyword[];\n listings: TapListing[];\n reviews: TapReviewsResult;\n crashes: TapCrashesResult;\n revenue: TapRevenueResult;\n /** `fixtureSeries`, so a series is built per metric rather than held. */\n series(metric: TapSeriesMetric): TapSeriesResult;\n}\n\n/**\n * The sandbox data the mock bridge and the local preview both answer with, so a\n * component behaves the same in a test and in the preview.\n *\n * @remarks\n * These are the same objects the `fixture*` exports hold, gathered under one name.\n * `answerFixtureQuery` clones before it answers, but reading `fixtures` directly\n * hands you the live object, so a test that sorts or splices one has to copy it\n * first.\n *\n * @example\n * ```ts\n * import { fixtures } from '@tappify/extension-sdk/testing';\n *\n * const newest = [...fixtures.releases].sort((left, right) =>\n * right.version.localeCompare(left.version),\n * )[0];\n * ```\n */\nexport const fixtures: TapFixtures = {\n project: fixtureProject,\n apps: fixtureApps,\n releases: fixtureReleases,\n keywords: fixtureKeywords,\n listings: fixtureListings,\n reviews: fixtureReviews,\n crashes: fixtureCrashes,\n revenue: fixtureRevenue,\n series: fixtureSeries,\n};\n\n/**\n * Answers one `tap.data.query` from the fixtures, which is what the mock bridge\n * does.\n *\n * @remarks\n * Every branch is cloned with `structuredClone`, so a component that sorts or\n * splices a result cannot reach the fixture the next test reads. It checks no\n * scope — `createTapMock` refuses an ungranted kind before it gets here — and it\n * ignores the range and platform in the query, so the numbers are the same\n * whatever the filter bar says. The seven assertions inside are the one place\n * TypeScript cannot follow the distributive conditional in `TapResult<Q>` back\n * through a `switch`; each branch returns exactly the result its `case` selected.\n *\n * @example\n * ```ts\n * import { answerFixtureQuery } from '@tappify/extension-sdk/testing';\n *\n * const reviews = answerFixtureQuery({\n * kind: 'reviews',\n * range: { from: '2026-09-01', to: '2026-09-08' },\n * });\n * const lowest = Math.min(...reviews.reviews.map(review => review.rating));\n * ```\n */\nexport function answerFixtureQuery<Q extends TapQuery>(query: Q): TapResult<Q> {\n switch (query.kind) {\n case 'project':\n return structuredClone(fixtureProject) as TapResult<Q>;\n case 'series':\n return structuredClone(fixtureSeries(query.metric)) as TapResult<Q>;\n case 'keywords':\n return structuredClone({ keywords: fixtureKeywords }) as TapResult<Q>;\n case 'listing':\n return structuredClone({ listings: fixtureListings }) as TapResult<Q>;\n case 'reviews':\n return structuredClone(fixtureReviews) as TapResult<Q>;\n case 'crashes':\n return structuredClone(fixtureCrashes) as TapResult<Q>;\n case 'revenue':\n return structuredClone(fixtureRevenue) as TapResult<Q>;\n }\n}\n","import {\n exportJWK,\n generateKeyPair,\n SignJWT,\n type CryptoKey,\n type JSONWebKeySet,\n} from 'jose';\nimport { TapError } from '../client/errors';\nimport type { TapFilters } from '../client/types';\nimport { createTappifyHandler } from '../server/handler';\nimport type {\n ContextBlock,\n MentionItems,\n MetricsInput,\n MetricsResponse,\n TappifyClaims,\n TappifyHandlerOptions,\n TappifyHealth,\n TappifyInstall,\n} from '../server/types';\nimport type { WorkOperationMap } from '../server/work';\nimport {\n callEnvelope,\n DEFAULT_FILTERS,\n readEnvelopeResponse,\n} from './envelope';\n\nconst TEST_KID = 'tappify-test-key';\nconst TEST_ISSUER = 'https://api.tappify.ai';\n\ninterface TestKeys {\n privateKey: CryptoKey;\n jwks: JSONWebKeySet;\n}\n\nlet keys: Promise<TestKeys> | null = null;\n\nfunction ensureKeys(): Promise<TestKeys> {\n keys ??= (async () => {\n const pair = await generateKeyPair('RS256', { extractable: true });\n const publicJwk = await exportJWK(pair.publicKey);\n return {\n privateKey: pair.privateKey,\n jwks: {\n keys: [{ ...publicJwk, kid: TEST_KID, alg: 'RS256', use: 'sig' }],\n },\n };\n })();\n\n return keys;\n}\n\n/**\n * Returns the public key set that verifies the tokens `signTestToken` signs.\n *\n * @remarks\n * The pair is generated once per process and cached, so every call in a suite\n * returns the same set. Pass it as `jwks` to `createTappifyHandler` when a test\n * drives the handler itself; `createTestClient` does that for you.\n *\n * @example\n * ```ts\n * import { createTappifyHandler } from '@tappify/extension-sdk/server';\n * import { testJwks } from '@tappify/extension-sdk/testing';\n *\n * async function buildHandler() {\n * return createTappifyHandler({ extensionId: 'starter', jwks: await testJwks() });\n * }\n * ```\n */\nexport async function testJwks(): Promise<JSONWebKeySet> {\n return (await ensureKeys()).jwks;\n}\n\n/**\n * Signs an install token a handler built against `testJwks` accepts.\n *\n * @remarks\n * Every claim has a default — install `ins_test`, extension `starter`, project\n * `prj_north`, organization `org_test`, no scopes — and the audience is\n * `ext:<extensionId>`, taken from `options.extensionId`, then from the claims, then\n * `starter`. It expires in five minutes unless `expiresIn` says otherwise, which is\n * how a test covers an expired token. `createTestClient` and `createTapMock` sign\n * their own tokens, so reach for this only when a test verifies a token itself.\n *\n * @example\n * ```ts\n * import { verifyTappifyToken } from '@tappify/extension-sdk/server';\n * import { signTestToken, testJwks } from '@tappify/extension-sdk/testing';\n *\n * async function claimsOfTestToken() {\n * const token = await signTestToken({ scopes: ['analytics:read'] });\n * return verifyTappifyToken(token, {\n * extensionId: 'starter',\n * jwks: await testJwks(),\n * });\n * }\n * ```\n */\nexport async function signTestToken(\n claims: Partial<TappifyClaims> = {},\n options: { extensionId?: string; expiresIn?: string } = {},\n): Promise<string> {\n const { privateKey } = await ensureKeys();\n const extensionId = options.extensionId ?? claims.extensionId ?? 'starter';\n\n return new SignJWT({\n installId: claims.installId ?? 'ins_test',\n extensionId,\n projectId: claims.projectId ?? 'prj_north',\n organizationId: claims.organizationId ?? 'org_test',\n scopes: claims.scopes ?? [],\n ...(claims.userId === undefined ? {} : { userId: claims.userId }),\n })\n .setProtectedHeader({ alg: 'RS256', kid: TEST_KID })\n .setIssuer(TEST_ISSUER)\n .setAudience(`ext:${extensionId}`)\n .setIssuedAt()\n .setExpirationTime(options.expiresIn ?? '5m')\n .sign(privateKey);\n}\n\n/** What every call a `TestClient` makes carries, unless a call overrides it. */\nexport interface TestClientDefaults {\n /** Merged over the default install, whose id is `ins_test`. */\n install?: Partial<TappifyInstall>;\n /** The scopes the token claims. Defaults to none. */\n scopes?: string[];\n /** What the handler reads as `credentials`. Defaults to empty. */\n credentials?: Record<string, unknown>;\n /** What the handler reads as `documents`, including `settings`. */\n documents?: Record<string, unknown>;\n /** The filter bar behind the call. Defaults to a seven-day September range. */\n filters?: TapFilters;\n /** One fixed idempotency key. Left out, each call gets its own. */\n eventId?: string;\n /** The origin the request url is built on. Defaults to `https://vendor.test`. */\n origin?: string;\n}\n\n/** One method per route Tappify calls, plus `raw` for the response itself. */\nexport interface TestClient {\n /** Calls the health route, which carries no token. */\n health(): Promise<TappifyHealth>;\n /** Calls the connector's metrics route. */\n metrics(input: MetricsInput): Promise<MetricsResponse>;\n /** Calls one tool by id and returns the card it answered with. */\n tool(id: string, input: unknown): Promise<unknown>;\n /** Calls one mention by id, with the query as the `q` parameter. */\n mention(id: string, query: string): Promise<MentionItems>;\n /** Calls one action by id. */\n action(id: string, input: unknown): Promise<unknown>;\n /** Calls one procedure by name; the input defaults to an empty object. */\n procedure(name: string, input?: unknown): Promise<unknown>;\n /** Calls one context block by id. */\n context(id: string): Promise<ContextBlock>;\n /** Posts one Tappify event, which the handler answers with 204. */\n event(name: string, payload: Record<string, unknown>): Promise<void>;\n /** Calls one work operation. */\n work<O extends keyof WorkOperationMap>(\n operation: O,\n input: WorkOperationMap[O]['input'],\n ): Promise<WorkOperationMap[O]['output']>;\n /** @deprecated Call set_status or describe_container using the current work protocol. */\n work(operation: 'status' | 'resolve', input: unknown): Promise<unknown>;\n /**\n * Sends any method and path and returns the `Response`, for a test that asserts\n * on a status rather than a body.\n */\n raw(method: string, path: string, body?: unknown): Promise<Response>;\n}\n\nconst HEALTH_PATH = '/tappify/health';\n\n/**\n * Drives a vendor server the way Tappify does, so a route is tested without a\n * running Tappify.\n *\n * @remarks\n * Pass the options object you hand `createTappifyHandler`, not the handler it\n * returns: the client rebuilds the handler around `testJwks` and signs each call\n * with a matching token. A built handler throws a `TapError` carrying\n * `TAP_TEST_TARGET_INVALID`, because it would verify against the live Tappify key\n * set and answer 401. Every call carries a fresh event id and the envelope Tappify\n * sends, and every call but `health` a signed token, so the handler reads the same\n * `install`, `context`, `credentials` and `documents` it will read in production. A route that\n * answers an error rejects with a `TapServerError` carrying the code, message and\n * status from the body, which is what a test asserts on; use `raw` when the status\n * itself is the assertion. Every method calls the unprefixed path — `/tappify/…` —\n * whatever `basePath` the options carry, so a `basePath` the handler strips off\n * something else is not exercised here, and one that is itself a prefix of\n * `/tappify`, such as `/tap`, makes every call 404 `TAP_ROUTE_UNKNOWN`. Reach a\n * prefixed path through `raw` when that is what you need to cover.\n *\n * @example\n * ```ts\n * import type { TappifyHandlerOptions } from '@tappify/extension-sdk/server';\n * import { createTestClient } from '@tappify/extension-sdk/testing';\n * import { expect, it } from 'vitest';\n *\n * const handlerOptions: TappifyHandlerOptions = {\n * extensionId: 'starter',\n * health: () => ({ ok: true, version: '0.1.0' }),\n * };\n * const client = createTestClient(handlerOptions, { scopes: ['analytics:read'] });\n *\n * it('answers the health check', async () => {\n * await expect(client.health()).resolves.toEqual({ ok: true, version: '0.1.0' });\n * });\n * ```\n */\nexport function createTestClient(\n target: TappifyHandlerOptions,\n defaults: TestClientDefaults = {},\n): TestClient {\n if (typeof target === 'function') {\n throw new TapError(\n 'TAP_TEST_TARGET_INVALID',\n 'createTestClient() was handed a built handler instead of the options object. A handler from createTappifyHandler() verifies install tokens against the live Tappify key set, so every call the test client makes would come back 401. Pass the options themselves — createTestClient(handlerOptions) — or, when you need the handler, build it with createTappifyHandler({ ...handlerOptions, jwks: await testJwks() }) and drive it through createTapMock({ handler, extensionId: handlerOptions.extensionId }) so the test token carries the audience the handler expects.',\n );\n }\n\n const extensionId = target.extensionId;\n const handlerPromise = testJwks().then(jwks =>\n createTappifyHandler({ ...target, jwks }),\n );\n\n const install: TappifyInstall = {\n id: 'ins_test',\n extensionId,\n projectId: 'prj_north',\n organizationId: 'org_test',\n ...defaults.install,\n };\n\n const filters = defaults.filters ?? DEFAULT_FILTERS;\n const origin = defaults.origin ?? 'https://vendor.test';\n\n let sent = 0;\n\n function envelope(input: unknown, event?: unknown): string {\n return callEnvelope({\n install,\n filters,\n input,\n credentials: defaults.credentials,\n documents: defaults.documents,\n event,\n });\n }\n\n async function send(\n method: string,\n path: string,\n body?: string,\n ): Promise<Response> {\n const handler = await handlerPromise;\n sent += 1;\n const headers = new Headers({\n 'content-type': 'application/json',\n 'x-tappify-event-id': defaults.eventId ?? `evt_${path}_${String(sent)}`,\n });\n\n if (path !== HEALTH_PATH) {\n const token = await signTestToken(\n {\n installId: install.id,\n extensionId: install.extensionId,\n projectId: install.projectId,\n organizationId: install.organizationId,\n scopes: defaults.scopes ?? [],\n },\n { extensionId },\n );\n headers.set('authorization', `Bearer ${token}`);\n }\n\n return handler(\n new Request(`${origin}${path}`, {\n method,\n headers,\n ...(body === undefined ? {} : { body }),\n }),\n );\n }\n\n async function call(\n method: string,\n path: string,\n body?: string,\n ): Promise<unknown> {\n return readEnvelopeResponse(\n await send(method, path, body),\n `${method} ${path}`,\n );\n }\n\n return {\n health: async () => (await call('GET', HEALTH_PATH)) as TappifyHealth,\n metrics: async input =>\n (await call(\n 'POST',\n '/tappify/metrics',\n envelope(input),\n )) as MetricsResponse,\n tool: (id, input) => call('POST', `/tappify/tools/${id}`, envelope(input)),\n mention: async (id, query) =>\n (await call(\n 'GET',\n `/tappify/mentions/${id}?q=${encodeURIComponent(query)}`,\n )) as MentionItems,\n action: (id, input) =>\n call('POST', `/tappify/actions/${id}`, envelope(input)),\n procedure: (name, input = {}) =>\n call('POST', `/tappify/procedures/${name}`, envelope(input)),\n context: async id =>\n (await call('GET', `/tappify/context/${id}`)) as ContextBlock,\n event: async (name, payload) => {\n await call('POST', '/tappify/events', envelope({}, { name, payload }));\n },\n work: (async (operation: string, input: unknown) =>\n call(\n 'POST',\n `/tappify/work/${operation}`,\n envelope(input),\n )) as TestClient['work'],\n raw: (method, path, body) =>\n send(method, path, body === undefined ? undefined : JSON.stringify(body)),\n };\n}\n","import { TapServerError } from '../client/errors';\nimport type { TapFilters } from '../client/types';\nimport { SERVER_ERROR_CODES } from '../server/codes';\nimport type { TappifyCallContext, TappifyInstall } from '../server/types';\nimport { readRecord, readText } from '../server/wire';\n\nexport const DEFAULT_FILTERS: TapFilters = {\n range: { from: '2026-09-01', to: '2026-09-08', preset: '7d' },\n platform: 'all',\n country: 'all',\n};\n\nexport interface CallEnvelope {\n install: TappifyInstall;\n filters: TapFilters;\n input: unknown;\n credentials?: Record<string, unknown>;\n documents?: Record<string, unknown>;\n event?: unknown;\n}\n\nexport function callEnvelope(envelope: CallEnvelope): string {\n const context: TappifyCallContext = {\n projectId: envelope.install.projectId,\n platform: envelope.filters.platform,\n from: envelope.filters.range.from,\n to: envelope.filters.range.to,\n filters: envelope.filters,\n };\n\n return JSON.stringify({\n install: envelope.install,\n context,\n input: envelope.input,\n credentials: envelope.credentials ?? {},\n documents: envelope.documents ?? {},\n event: envelope.event,\n });\n}\n\nfunction parseJson(text: string): { parsed: boolean; value: unknown } {\n try {\n return { parsed: true, value: JSON.parse(text) as unknown };\n } catch {\n return { parsed: false, value: undefined };\n }\n}\n\nexport async function readEnvelopeResponse(\n response: Response,\n call: string,\n): Promise<unknown> {\n const text = await response.text();\n const body = parseJson(text);\n const status = String(response.status);\n\n if (response.ok) {\n if (body.parsed) return body.value;\n if (text.trim() === '') return undefined;\n\n throw new TapServerError(\n SERVER_ERROR_CODES.INTERNAL_ERROR,\n `The extension server answered ${status} for ${call} with a body that is not JSON. Every Tappify route answers JSON, or 204 with no body.`,\n response.status,\n );\n }\n\n const error = readRecord(body.value, 'error');\n\n throw new TapServerError(\n readText(error, 'code') ?? SERVER_ERROR_CODES.INTERNAL_ERROR,\n readText(error, 'message') ??\n `The extension server answered ${status} for ${call} without a JSON error body.`,\n response.status,\n );\n}\n","import { TapError } from '../client/errors';\nimport type {\n Tap,\n TapChangeKey,\n TapChatCard,\n TapCollection,\n TapConfirmOptions,\n TapFilters,\n TapHostEvents,\n TapProcedures,\n TapSingleton,\n TapSize,\n TapStorage,\n TapToastTone,\n} from '../client/types';\nimport { QUERY_SCOPES, SCOPE_KEYS, type ScopeKey } from '../manifest/constants';\nimport type { TappifyFetchHandler } from '../server/handler';\nimport { signTestToken } from './client';\nimport {\n callEnvelope,\n DEFAULT_FILTERS,\n readEnvelopeResponse,\n} from './envelope';\nimport { answerFixtureQuery } from './fixtures';\n\n/**\n * Everything the component asked the host to do, in call order, for a test to\n * assert on.\n */\nexport interface TapMockCalls {\n /** One entry per `tap.nav.push`. */\n navigations: string[];\n /** One entry per `tap.nav.setSearch`. */\n searches: Record<string, string | null>[];\n /** One entry per `tap.ui.toast`; the tone defaults to `neutral`. */\n toasts: { message: string; tone: TapToastTone }[];\n /** One entry per `tap.ui.confirm`, whatever the mock answered. */\n confirms: TapConfirmOptions[];\n /** One entry per `tap.actions.run`, which the mock records rather than refuses. */\n actions: { actionId: string; input: unknown }[];\n /** One entry per `tap.telemetry.event`. */\n telemetry: {\n name: string;\n props?: Record<string, string | number | boolean>;\n }[];\n /** One entry per `tap.nav.openChat`. */\n chats: { prompt: string; context?: Record<string, unknown> }[];\n /** One url per `tap.nav.openExternal`, with no confirmation asked. */\n externals: string[];\n /** The filename and byte size per `tap.ui.download`, not the blob itself. */\n downloads: { filename: string; size: number }[];\n /** One entry per `tap.ui.copy`, with no clipboard touched. */\n copies: string[];\n /** One card per `tap.ui.openInChat`, unformatted. */\n cards: TapChatCard[];\n /** How many times `tap.ui.expand` was called. */\n expands: number;\n /** How many times `tap.nav.openSettings` was called. */\n settingsOpens: number;\n}\n\n/**\n * How the mock bridge is built, and the options `renderWithTap` forwards to it.\n */\nexport interface CreateTapMockOptions {\n /** Defaults to `ins_test`. */\n installId?: string;\n /** Defaults to `starter`, and has to match `handler`'s own extension id. */\n extensionId?: string;\n /** Defaults to `Starter`. */\n extensionName?: string;\n /** What the install granted. Defaults to every scope. */\n scopes?: ScopeKey[];\n /** What `tap.ui.size` reports. Defaults to `slot`. */\n size?: TapSize;\n /** Merged over the fixture filter bar. */\n filters?: Partial<TapFilters>;\n /** Merged over the fixture Northlight project. */\n project?: Partial<Tap['project']>;\n /** What `tap.context` holds. Defaults to empty. */\n context?: Record<string, unknown>;\n /** What `tap.params` holds, in the host's positional shape. Defaults to empty. */\n params?: Record<string, string>;\n /** What `useTapTheme` reports. Defaults to `light`. */\n theme?: 'light' | 'dark';\n /** What `tap.ui.confirm` resolves to. Defaults to `true`. */\n confirmAnswer?: boolean;\n /** The collections to expose, and the kind of each. Left out, every name works. */\n storage?: Record<string, 'singleton' | 'collection'>;\n /** One function per procedure, answering `tap.server.<name>` in process. */\n server?: Record<string, (input: unknown) => unknown>;\n /** Your own handler, so `tap.server.<name>` goes through its real routes. */\n handler?: TappifyFetchHandler;\n /** A token to send to `handler`. Defaults to one the mock signs itself. */\n handlerToken?: string;\n}\n\n/** The mock bridge, with the recorded calls and the handles that drive it. */\nexport interface TapMock {\n /** The bridge itself, which `renderWithTap` puts above the component. */\n tap: Tap;\n calls: TapMockCalls;\n /** The node SDK dialogs render into, appended to the document by `renderWithTap`. */\n portal: HTMLElement;\n /** What a collection holds now, or just the one document under `documentId`. */\n documents(collection: string, documentId?: string): unknown[];\n /** Delivers a Tappify event to everything subscribed through `tap.data.subscribe`. */\n emit<E extends keyof TapHostEvents>(\n event: E,\n payload: TapHostEvents[E],\n ): void;\n /** Replaces the whole filter bar and re-renders the hooks that read it. */\n setFilters(filters: TapFilters): void;\n /** Moves the mount between slot, panel and page. */\n setSize(size: TapSize): void;\n /** Switches the theme `useTapTheme` reports. */\n setTheme(mode: 'light' | 'dark'): void;\n /** Replaces the route `useTapParams` reports, in the host's own positional shape. */\n setParams(params: Record<string, string>): void;\n}\n\ninterface StoredDocument {\n id: string;\n data: unknown;\n}\n\ntype StorageHandle = TapSingleton<unknown> & TapCollection<unknown>;\n\ntype ExposedHandle = TapSingleton<unknown> | TapCollection<unknown>;\n\nconst SINGLETON_ID = 'singleton';\n\nconst PROBE_KEYS = new Set([\n 'then',\n 'toJSON',\n '$$typeof',\n 'constructor',\n 'prototype',\n 'valueOf',\n 'toString',\n 'inspect',\n 'asymmetricMatch',\n 'nodeType',\n]);\n\nfunction unknownCollectionError(collection: string): TapError {\n return new TapError(\n 'TAP_UNKNOWN_COLLECTION',\n `This mock exposes no storage collection called \"${collection}\". createTapMock({ storage: … }) was given an explicit map, so only the collections it names exist. Add \"${collection}\" to that map, or drop the storage option to expose every collection.`,\n );\n}\n\nfunction singletonView(handle: StorageHandle): TapSingleton<unknown> {\n return {\n get: () => handle.get(),\n set: document => handle.set(document),\n patch: partial => handle.patch(partial),\n };\n}\n\nfunction collectionView(handle: StorageHandle): TapCollection<unknown> {\n return {\n list: listOptions => handle.list(listOptions),\n get: id => handle.get(id),\n put: (id, document) => handle.put(id, document),\n delete: id => handle.delete(id),\n };\n}\n\nfunction scopeError(scope: ScopeKey): TapError {\n return new TapError(\n 'TAP_SCOPE_MISSING',\n `This install has not granted ${scope}. Declare it under \"scopes\" in tappify.extension.json, or pass scopes: [\"${scope}\"] to renderWithTap() to test the granted path.`,\n );\n}\n\nfunction asObject(value: unknown): object {\n return typeof value === 'object' && value !== null ? value : {};\n}\n\n/**\n * Builds the bridge a component talks to in a test, without rendering anything.\n *\n * @remarks\n * Data queries answer from the sandbox fixtures, storage is an in-memory store, and\n * everything else the host would do is recorded in `calls`. Scopes default to all of\n * them: pass `scopes` to test the refused path, where a data query rejects with\n * `TAP_SCOPE_MISSING` for the kind's own scope and every storage call, reads\n * included, needs `storage:write`. A procedure needs either `server`, which answers\n * in process, or `handler`, which is called over a real `Request` with a token the\n * mock signs — so that handler has to be built with `jwks: await testJwks()` and the\n * same `extensionId` — and without either, `tap.server.<name>` rejects with\n * `TAP_UNKNOWN_PROCEDURE`. With `storage` given, a collection the map does not name\n * throws `TAP_UNKNOWN_COLLECTION`. Unlike the host, this mock records\n * `tap.actions.run` and resolves it as pending rather than refusing it, so a test\n * can assert on the call today. Use `renderWithTap` when there is a component to\n * render.\n *\n * @example\n * ```ts\n * import { createTapMock } from '@tappify/extension-sdk/testing';\n * import { expect, it } from 'vitest';\n *\n * it('answers a keyword query from the fixtures', async () => {\n * const mock = createTapMock({ scopes: ['store.metadata:read'] });\n * const keywords = await mock.tap.data.query({ kind: 'keywords' });\n * expect(keywords.keywords).toHaveLength(3);\n * });\n * ```\n */\nexport function createTapMock(options: CreateTapMockOptions = {}): TapMock {\n const granted = new Set<ScopeKey>(options.scopes ?? SCOPE_KEYS);\n const listeners = new Map<TapChangeKey, Set<() => void>>();\n const eventHandlers = new Map<string, Set<(payload: unknown) => void>>();\n const state = new Map<string, unknown>();\n const store = new Map<string, StoredDocument[]>();\n\n const portal = document.createElement('div');\n portal.dataset.tapPortal = 'true';\n\n const calls: TapMockCalls = {\n navigations: [],\n searches: [],\n toasts: [],\n confirms: [],\n actions: [],\n telemetry: [],\n chats: [],\n externals: [],\n downloads: [],\n copies: [],\n cards: [],\n expands: 0,\n settingsOpens: 0,\n };\n\n const emitChange = (key: TapChangeKey): void => {\n for (const listener of listeners.get(key) ?? []) listener();\n };\n\n const subscribe = (key: TapChangeKey, listener: () => void): (() => void) => {\n const set = listeners.get(key) ?? new Set<() => void>();\n set.add(listener);\n listeners.set(key, set);\n return () => {\n set.delete(listener);\n };\n };\n\n const withStorage = <T>(run: () => T): Promise<T> =>\n granted.has('storage:write')\n ? Promise.resolve(run())\n : Promise.reject(scopeError('storage:write'));\n\n const bucket = (collection: string): StoredDocument[] => {\n const existing = store.get(collection);\n if (existing) return existing;\n const created: StoredDocument[] = [];\n store.set(collection, created);\n return created;\n };\n\n function buildHandle(collection: string): StorageHandle {\n const find = (id: string): StoredDocument | undefined =>\n bucket(collection).find(entry => entry.id === id);\n\n const write = (id: string, data: unknown): void => {\n const items = bucket(collection);\n const index = items.findIndex(entry => entry.id === id);\n if (index === -1) items.push({ id, data });\n else items[index] = { id, data };\n emitChange(`storage:${collection}`);\n };\n\n return {\n get: (id?: string) =>\n withStorage(() => find(id ?? SINGLETON_ID)?.data ?? null),\n set: document =>\n withStorage(() => {\n write(SINGLETON_ID, document);\n }),\n patch: partial =>\n withStorage(() => {\n const merged = { ...asObject(find(SINGLETON_ID)?.data), ...partial };\n write(SINGLETON_ID, merged);\n return merged;\n }),\n list: () =>\n withStorage(() => ({\n items: bucket(collection)\n .filter(entry => entry.id !== SINGLETON_ID)\n .map(entry => ({ ...asObject(entry.data), id: entry.id })),\n })),\n put: (id, document) =>\n withStorage(() => {\n write(id, document);\n }),\n delete: id =>\n withStorage(() => {\n const items = bucket(collection);\n const index = items.findIndex(entry => entry.id === id);\n if (index !== -1) {\n items.splice(index, 1);\n emitChange(`storage:${collection}`);\n }\n }),\n };\n }\n\n const handles = new Map<string, ExposedHandle>();\n const kinds = options.storage;\n\n const exposeHandle = (collection: string): ExposedHandle => {\n const kind = kinds?.[collection];\n if (kinds !== undefined && kind === undefined) {\n throw unknownCollectionError(collection);\n }\n\n const handle = buildHandle(collection);\n if (kind === 'singleton') return singletonView(handle);\n if (kind === 'collection') return collectionView(handle);\n return handle;\n };\n\n const handleFor = (collection: string): ExposedHandle | undefined => {\n if (PROBE_KEYS.has(collection)) return undefined;\n\n const existing = handles.get(collection);\n if (existing) return existing;\n const created = exposeHandle(collection);\n handles.set(collection, created);\n return created;\n };\n\n const storageTarget: TapStorage = {};\n const storage = new Proxy(storageTarget, {\n get: (_target, property) =>\n typeof property === 'string' ? handleFor(property) : undefined,\n has: () => true,\n ownKeys: () => [...store.keys()],\n getOwnPropertyDescriptor: () => ({ enumerable: true, configurable: true }),\n });\n\n let calledProcedures = 0;\n let signedToken: Promise<string> | null = null;\n\n function handlerToken(): Promise<string> {\n if (options.handlerToken !== undefined) {\n return Promise.resolve(options.handlerToken);\n }\n signedToken ??= signTestToken(\n {\n installId: tap.extension.installId,\n extensionId: tap.extension.id,\n projectId: tap.extension.projectId,\n organizationId: tap.auth.organizationId,\n scopes: [...granted],\n },\n { extensionId: tap.extension.id },\n );\n return signedToken;\n }\n\n async function callProcedure(name: string, input: unknown): Promise<unknown> {\n const local = options.server?.[name];\n if (local) return local(input);\n\n if (options.handler) {\n const path = `/tappify/procedures/${name}`;\n calledProcedures += 1;\n const response = await options.handler(\n new Request(`https://vendor.test${path}`, {\n method: 'POST',\n headers: {\n 'content-type': 'application/json',\n 'x-tappify-event-id': `evt_${name}_${String(calledProcedures)}`,\n authorization: `Bearer ${await handlerToken()}`,\n },\n body: callEnvelope({\n install: {\n id: tap.extension.installId,\n extensionId: tap.extension.id,\n projectId: tap.extension.projectId,\n organizationId: tap.auth.organizationId,\n },\n filters: tap.filters,\n input,\n }),\n }),\n );\n\n return readEnvelopeResponse(response, `POST ${path}`);\n }\n\n throw new TapError(\n 'TAP_UNKNOWN_PROCEDURE',\n `No handler is registered for the procedure \"${name}\". Pass server: { ${name}: input => … } or handler: <your createTappifyHandler handler> to createTapMock().`,\n );\n }\n\n const serverTarget: TapProcedures = {};\n\n const tap: Tap = {\n extension: {\n id: options.extensionId ?? 'starter',\n name: options.extensionName ?? 'Starter',\n installId: options.installId ?? 'ins_test',\n projectId: 'prj_north',\n },\n env: 'dev',\n host: { version: 'test', sdkMajors: [2] },\n auth: {\n token: 'test-token',\n user: { id: 'usr_test', role: 'admin' },\n organizationId: 'org_test',\n scopes: [...granted],\n can: scope => granted.has(scope),\n },\n project: {\n id: 'prj_north',\n name: 'Northlight',\n platforms: ['ios', 'android'],\n apps: [],\n releases: [],\n keywords: [],\n ...options.project,\n },\n filters: { ...DEFAULT_FILTERS, ...options.filters },\n theme: { mode: options.theme ?? 'light' },\n locale: 'en-US',\n timezone: 'UTC',\n format: {\n number: (value, formatOptions) =>\n new Intl.NumberFormat('en-US', formatOptions).format(value),\n currency: (value, code) =>\n new Intl.NumberFormat('en-US', {\n style: 'currency',\n currency: code,\n }).format(value),\n date: value => new Date(value).toISOString().slice(0, 10),\n relative: value => new Date(value).toISOString(),\n },\n nav: {\n push: path => calls.navigations.push(path),\n setSearch: params => calls.searches.push(params),\n openSettings: () => {\n calls.settingsOpens += 1;\n },\n openChat: (prompt, context) => calls.chats.push({ prompt, context }),\n openExternal: url => calls.externals.push(url),\n },\n data: {\n query: query => {\n const scope = QUERY_SCOPES[query.kind];\n if (!granted.has(scope)) return Promise.reject(scopeError(scope));\n return Promise.resolve(answerFixtureQuery(query));\n },\n subscribe: <E extends keyof TapHostEvents>(\n event: E,\n handler: (payload: TapHostEvents[E]) => void,\n ): (() => void) => {\n const key = String(event);\n const set = eventHandlers.get(key) ?? new Set<(p: unknown) => void>();\n const wrapped = (payload: unknown): void => {\n handler(payload as TapHostEvents[E]);\n };\n set.add(wrapped);\n eventHandlers.set(key, set);\n return () => {\n set.delete(wrapped);\n };\n },\n },\n server: new Proxy(serverTarget, {\n get:\n (_target, property) =>\n (input: unknown): Promise<unknown> =>\n callProcedure(String(property), input),\n has: () => true,\n }),\n state: {\n get: <T>(key: string): T | undefined => {\n const value = state.get(key);\n return value === undefined ? undefined : (value as T);\n },\n set: (key, value) => {\n state.set(key, value);\n emitChange(`state:${key}`);\n },\n subscribe: (key, handler) => subscribe(`state:${key}`, handler),\n },\n ui: {\n size: options.size ?? 'slot',\n toast: (message, tone = 'neutral') =>\n calls.toasts.push({ message, tone }),\n confirm: confirmOptions => {\n calls.confirms.push(confirmOptions);\n return Promise.resolve(options.confirmAnswer ?? true);\n },\n openInChat: card => calls.cards.push(card),\n expand: () => {\n calls.expands += 1;\n },\n download: (blob, filename) =>\n calls.downloads.push({ filename, size: blob.size }),\n copy: text => {\n calls.copies.push(text);\n return Promise.resolve();\n },\n },\n context: options.context ?? {},\n params: options.params ?? {},\n storage,\n telemetry: {\n event: (name, props) => calls.telemetry.push({ name, props }),\n },\n actions: {\n run: (actionId, input) => {\n calls.actions.push({ actionId, input });\n return Promise.resolve({\n id: `run_${String(calls.actions.length)}`,\n actionId,\n status: 'pending' as const,\n requestedAt: new Date(0).toISOString(),\n });\n },\n },\n invalidate: () => undefined,\n __portal: portal,\n __subscribe: subscribe,\n };\n\n return {\n tap,\n calls,\n portal,\n documents: (collection, documentId) =>\n bucket(collection)\n .filter(entry => documentId === undefined || entry.id === documentId)\n .map(entry => entry.data),\n emit: (event, payload) => {\n for (const handler of eventHandlers.get(String(event)) ?? []) {\n handler(payload);\n }\n },\n setFilters: filters => {\n tap.filters = filters;\n emitChange('filters');\n },\n setSize: size => {\n tap.ui = { ...tap.ui, size };\n emitChange('size');\n },\n setTheme: mode => {\n tap.theme = { mode };\n emitChange('theme');\n },\n setParams: params => {\n tap.params = params;\n emitChange('params');\n },\n };\n}\n"]}
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  EVENTS
3
- } from "./chunk-SGCYEEXD.mjs";
3
+ } from "./chunk-IHLK4I7J.mjs";
4
4
  import {
5
5
  TapError,
6
6
  TapServerError
@@ -375,4 +375,4 @@ export {
375
375
  readText,
376
376
  createTappifyHandler
377
377
  };
378
- //# sourceMappingURL=chunk-KZ5NGFUP.mjs.map
378
+ //# sourceMappingURL=chunk-CEFLOQD4.mjs.map