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

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 (148) hide show
  1. package/README.md +208 -0
  2. package/dist/chunk-2CY5HVY7.mjs +59 -0
  3. package/dist/chunk-2CY5HVY7.mjs.map +1 -0
  4. package/dist/{chunk-2DBSVKAU.cjs → chunk-6HETNZJY.cjs} +1 -1
  5. package/dist/chunk-6HETNZJY.cjs.map +1 -0
  6. package/dist/{chunk-PYJMJF44.cjs → chunk-7YKH4LZN.cjs} +9 -2
  7. package/dist/chunk-7YKH4LZN.cjs.map +1 -0
  8. package/dist/{chunk-RMUWRESK.cjs → chunk-A74HCO6T.cjs} +31 -18
  9. package/dist/chunk-A74HCO6T.cjs.map +1 -0
  10. package/dist/chunk-BPCGXXMD.cjs +781 -0
  11. package/dist/chunk-BPCGXXMD.cjs.map +1 -0
  12. package/dist/chunk-EB5XWQAU.cjs +132 -0
  13. package/dist/chunk-EB5XWQAU.cjs.map +1 -0
  14. package/dist/{chunk-5AAPD5WV.cjs → chunk-GAUZYBXE.cjs} +11 -11
  15. package/dist/chunk-GAUZYBXE.cjs.map +1 -0
  16. package/dist/chunk-J5LGTIGS.mjs +10 -0
  17. package/dist/chunk-J5LGTIGS.mjs.map +1 -0
  18. package/dist/{chunk-ZT35X7KL.mjs → chunk-KZ5NGFUP.mjs} +22 -9
  19. package/dist/chunk-KZ5NGFUP.mjs.map +1 -0
  20. package/dist/chunk-NN263PGQ.cjs +70 -0
  21. package/dist/chunk-NN263PGQ.cjs.map +1 -0
  22. package/dist/{chunk-3YWVYA42.cjs → chunk-O357OJXL.cjs} +14 -18
  23. package/dist/chunk-O357OJXL.cjs.map +1 -0
  24. package/dist/chunk-ONJMPO3P.cjs +59 -0
  25. package/dist/chunk-ONJMPO3P.cjs.map +1 -0
  26. package/dist/{chunk-OPPYEA7F.cjs → chunk-PRCZMOLD.cjs} +3 -3
  27. package/dist/chunk-PRCZMOLD.cjs.map +1 -0
  28. package/dist/chunk-Q7SFCCGT.cjs +10 -0
  29. package/dist/chunk-Q7SFCCGT.cjs.map +1 -0
  30. package/dist/{chunk-QHM7XNRK.mjs → chunk-QA5AL3VZ.mjs} +2 -2
  31. package/dist/{chunk-QHM7XNRK.mjs.map → chunk-QA5AL3VZ.mjs.map} +1 -1
  32. package/dist/chunk-QJMZRV7A.mjs +132 -0
  33. package/dist/chunk-QJMZRV7A.mjs.map +1 -0
  34. package/dist/{chunk-CSTYBKZC.mjs → chunk-QXD7WVKK.mjs} +1 -1
  35. package/dist/chunk-QXD7WVKK.mjs.map +1 -0
  36. package/dist/{chunk-4C5Z3CIK.mjs → chunk-RBIQT75L.mjs} +2 -2
  37. package/dist/chunk-RBIQT75L.mjs.map +1 -0
  38. package/dist/{chunk-MNDVYPEP.mjs → chunk-SGCYEEXD.mjs} +9 -2
  39. package/dist/chunk-SGCYEEXD.mjs.map +1 -0
  40. package/dist/{chunk-BDCX6LRQ.mjs → chunk-SGEYDV3K.mjs} +3 -3
  41. package/dist/chunk-SGEYDV3K.mjs.map +1 -0
  42. package/dist/{chunk-IGK2AP6J.mjs → chunk-SLGOHIBL.mjs} +13 -17
  43. package/dist/chunk-SLGOHIBL.mjs.map +1 -0
  44. package/dist/chunk-SWG7CNTZ.mjs +781 -0
  45. package/dist/chunk-SWG7CNTZ.mjs.map +1 -0
  46. package/dist/{chunk-7KIKOH5H.cjs → chunk-VV7ELNRW.cjs} +3 -3
  47. package/dist/chunk-VV7ELNRW.cjs.map +1 -0
  48. package/dist/chunk-Z43DPNZ6.mjs +70 -0
  49. package/dist/chunk-Z43DPNZ6.mjs.map +1 -0
  50. package/dist/client-BO3kavn0.d.ts +25 -0
  51. package/dist/client-DE-vMgdO.d.cts +25 -0
  52. package/dist/codegen.cjs.map +1 -1
  53. package/dist/codegen.d.cts +43 -7
  54. package/dist/codegen.d.ts +43 -7
  55. package/dist/codegen.mjs.map +1 -1
  56. package/dist/{constants-DJ_KgZpN.d.cts → constants-Bh759FUI.d.cts} +44 -8
  57. package/dist/{constants-DJ_KgZpN.d.ts → constants-Bh759FUI.d.ts} +44 -8
  58. package/dist/{context-Dt6xh_5t.d.cts → context-DI0EJCRD.d.cts} +5 -4
  59. package/dist/{context-Ml7k_V14.d.ts → context-mr8R5CNt.d.ts} +5 -4
  60. package/dist/{errors-COsBh7kd.d.cts → errors-Cqh0wrqt.d.cts} +3 -2
  61. package/dist/{errors-COsBh7kd.d.ts → errors-Cqh0wrqt.d.ts} +3 -2
  62. package/dist/host.cjs +5 -4
  63. package/dist/host.cjs.map +1 -1
  64. package/dist/host.d.cts +5 -5
  65. package/dist/host.d.ts +5 -5
  66. package/dist/host.mjs +4 -3
  67. package/dist/host.mjs.map +1 -1
  68. package/dist/index.cjs +9 -656
  69. package/dist/index.cjs.map +1 -1
  70. package/dist/index.d.cts +69 -627
  71. package/dist/index.d.ts +69 -627
  72. package/dist/index.mjs +5 -652
  73. package/dist/index.mjs.map +1 -1
  74. package/dist/manifest.cjs +142 -86
  75. package/dist/manifest.cjs.map +1 -1
  76. package/dist/manifest.d.cts +97 -12
  77. package/dist/manifest.d.ts +97 -12
  78. package/dist/manifest.mjs +129 -73
  79. package/dist/manifest.mjs.map +1 -1
  80. package/dist/server.cjs +15 -66
  81. package/dist/server.cjs.map +1 -1
  82. package/dist/server.d.cts +6 -6
  83. package/dist/server.d.ts +6 -6
  84. package/dist/server.mjs +10 -61
  85. package/dist/server.mjs.map +1 -1
  86. package/dist/testing/index.cjs +73 -750
  87. package/dist/testing/index.cjs.map +1 -1
  88. package/dist/testing/index.d.cts +30 -11
  89. package/dist/testing/index.d.ts +30 -11
  90. package/dist/testing/index.mjs +89 -766
  91. package/dist/testing/index.mjs.map +1 -1
  92. package/dist/testing/jest.cjs +6 -5
  93. package/dist/testing/jest.cjs.map +1 -1
  94. package/dist/testing/jest.mjs +3 -2
  95. package/dist/testing/jest.mjs.map +1 -1
  96. package/dist/testing/vitest.cjs +6 -5
  97. package/dist/testing/vitest.cjs.map +1 -1
  98. package/dist/testing/vitest.d.cts +10 -0
  99. package/dist/testing/vitest.d.ts +10 -0
  100. package/dist/testing/vitest.mjs +3 -2
  101. package/dist/testing/vitest.mjs.map +1 -1
  102. package/dist/{types-DDAGLpov.d.ts → types-BdDaLnqs.d.cts} +4 -3
  103. package/dist/{types-DrpQSv_2.d.cts → types-uGHrRaOj.d.ts} +4 -3
  104. package/dist/ui/styles.css +740 -0
  105. package/dist/ui.cjs +1018 -0
  106. package/dist/ui.cjs.map +1 -0
  107. package/dist/ui.d.cts +231 -0
  108. package/dist/ui.d.ts +231 -0
  109. package/dist/ui.mjs +1018 -0
  110. package/dist/ui.mjs.map +1 -0
  111. package/dist/vite-preview-bootstrap.cjs +66 -0
  112. package/dist/vite-preview-bootstrap.cjs.map +1 -0
  113. package/dist/vite-preview-bootstrap.d.cts +15 -0
  114. package/dist/vite-preview-bootstrap.d.ts +15 -0
  115. package/dist/vite-preview-bootstrap.mjs +66 -0
  116. package/dist/vite-preview-bootstrap.mjs.map +1 -0
  117. package/dist/vite-preview.cjs +876 -0
  118. package/dist/vite-preview.cjs.map +1 -0
  119. package/dist/vite-preview.d.cts +9 -0
  120. package/dist/vite-preview.d.ts +9 -0
  121. package/dist/vite-preview.mjs +876 -0
  122. package/dist/vite-preview.mjs.map +1 -0
  123. package/dist/vite.cjs +320 -60
  124. package/dist/vite.cjs.map +1 -1
  125. package/dist/vite.d.cts +54 -8
  126. package/dist/vite.d.ts +54 -8
  127. package/dist/vite.mjs +312 -54
  128. package/dist/vite.mjs.map +1 -1
  129. package/dist/work-events-BlOVpK0k.d.ts +547 -0
  130. package/dist/work-events-D97EoqvS.d.cts +547 -0
  131. package/package.json +19 -2
  132. package/schema/extension-v2.json +440 -121
  133. package/dist/chunk-2DBSVKAU.cjs.map +0 -1
  134. package/dist/chunk-3YWVYA42.cjs.map +0 -1
  135. package/dist/chunk-4C5Z3CIK.mjs.map +0 -1
  136. package/dist/chunk-5AAPD5WV.cjs.map +0 -1
  137. package/dist/chunk-7KIKOH5H.cjs.map +0 -1
  138. package/dist/chunk-BDCX6LRQ.mjs.map +0 -1
  139. package/dist/chunk-CSTYBKZC.mjs.map +0 -1
  140. package/dist/chunk-IGK2AP6J.mjs.map +0 -1
  141. package/dist/chunk-MNDVYPEP.mjs.map +0 -1
  142. package/dist/chunk-OPPYEA7F.cjs.map +0 -1
  143. package/dist/chunk-PYJMJF44.cjs.map +0 -1
  144. package/dist/chunk-RMUWRESK.cjs.map +0 -1
  145. package/dist/chunk-ZT35X7KL.mjs.map +0 -1
  146. package/dist/handler-CZIlZ5vu.d.ts +0 -227
  147. package/dist/handler-kU9Jf36K.d.cts +0 -227
  148. package/dist/styles.css +0 -367
@@ -0,0 +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"]}
@@ -0,0 +1,132 @@
1
+ "use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _nullishCoalesce(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return rhsFn(); } }
2
+
3
+
4
+
5
+
6
+ var _chunkA74HCO6Tcjs = require('./chunk-A74HCO6T.cjs');
7
+
8
+
9
+ var _chunk6HETNZJYcjs = require('./chunk-6HETNZJY.cjs');
10
+
11
+ // src/server/events.ts
12
+ var DEFAULT_BASE_URL = "https://api.tappify.ai";
13
+ function toHex(buffer) {
14
+ return Array.from(new Uint8Array(buffer)).map((byte) => byte.toString(16).padStart(2, "0")).join("");
15
+ }
16
+ async function signWebhook(secret, body) {
17
+ const encoder = new TextEncoder();
18
+ const key = await crypto.subtle.importKey(
19
+ "raw",
20
+ encoder.encode(secret),
21
+ { name: "HMAC", hash: "SHA-256" },
22
+ false,
23
+ ["sign"]
24
+ );
25
+ const signature = await crypto.subtle.sign("HMAC", key, encoder.encode(body));
26
+ return toHex(signature);
27
+ }
28
+ async function rejectionCode(response) {
29
+ try {
30
+ const payload = await response.json();
31
+ return _chunkA74HCO6Tcjs.readText.call(void 0, _chunkA74HCO6Tcjs.readRecord.call(void 0, payload, "error"), "code");
32
+ } catch (e) {
33
+ return null;
34
+ }
35
+ }
36
+ async function sendEvent(name, payload, options) {
37
+ const envelope = {
38
+ event: name,
39
+ source: options.extensionId,
40
+ occurredAt: _nullishCoalesce(options.occurredAt, () => ( (/* @__PURE__ */ new Date()).toISOString())),
41
+ dedupeKey: _nullishCoalesce(options.dedupeKey, () => ( crypto.randomUUID())),
42
+ payload: { ...payload }
43
+ };
44
+ if (options.build !== void 0) envelope.build = options.build;
45
+ const body = JSON.stringify(envelope);
46
+ const signature = await signWebhook(options.secret, body);
47
+ const url = _nullishCoalesce(options.endpoint, () => ( `${_nullishCoalesce(options.baseUrl, () => ( DEFAULT_BASE_URL))}/api/v1/extensions/hooks/${options.extensionId}/${options.installId}`));
48
+ const send = _nullishCoalesce(options.fetch, () => ( fetch));
49
+ const response = await send(url, {
50
+ method: "POST",
51
+ headers: {
52
+ "content-type": "application/json",
53
+ "x-tappify-signature": signature
54
+ },
55
+ body
56
+ });
57
+ if (!response.ok) {
58
+ const rejection = await rejectionCode(response);
59
+ throw new (0, _chunk6HETNZJYcjs.TapServerError)(
60
+ _chunkA74HCO6Tcjs.SERVER_ERROR_CODES.WEBHOOK_REJECTED,
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
+ response.status
63
+ );
64
+ }
65
+ }
66
+
67
+ // src/server/work-events.ts
68
+ function nonempty(value) {
69
+ return typeof value === "string" && value.trim().length > 0;
70
+ }
71
+ function validEvent(event) {
72
+ const type = _chunkA74HCO6Tcjs.readText.call(void 0, event, "type");
73
+ if (!["comment.created", "status.changed", "item.deleted"].includes(_nullishCoalesce(type, () => ( ""))))
74
+ return false;
75
+ if (!["externalItemId", "providerEventId", "occurredAt", "dedupeKey"].every(
76
+ (key) => nonempty(_chunkA74HCO6Tcjs.readUnknown.call(void 0, event, key))
77
+ ))
78
+ 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");
82
+ if (reference !== void 0)
83
+ return nonempty(reference) && comment === void 0 && status === void 0;
84
+ if (type === "item.deleted")
85
+ return comment === void 0 && status === void 0;
86
+ 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);
89
+ }
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);
93
+ }
94
+ async function sendWorkEvent(event, options) {
95
+ if (!validEvent(event))
96
+ throw new TypeError(
97
+ "Invalid work event: provide complete normalized content or a nonempty hydrateRef, with routing and identity fields."
98
+ );
99
+ if (!nonempty(options.callbackUrl))
100
+ throw new TypeError(
101
+ "A work event requires the callbackUrl provisioned through configure."
102
+ );
103
+ const body = JSON.stringify(event);
104
+ const signature = await signWebhook(options.secret, body);
105
+ const response = await (_nullishCoalesce(options.fetch, () => ( fetch)))(options.callbackUrl, {
106
+ method: "POST",
107
+ headers: {
108
+ "content-type": "application/json",
109
+ "x-tappify-signature": signature
110
+ },
111
+ body
112
+ });
113
+ if (!response.ok) {
114
+ let rejection = null;
115
+ try {
116
+ rejection = _chunkA74HCO6Tcjs.readText.call(void 0, _chunkA74HCO6Tcjs.readRecord.call(void 0, await response.json(), "error"), "code");
117
+ } catch (e2) {
118
+ }
119
+ throw new (0, _chunk6HETNZJYcjs.TapServerError)(
120
+ _chunkA74HCO6Tcjs.SERVER_ERROR_CODES.WEBHOOK_REJECTED,
121
+ `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
+ response.status
123
+ );
124
+ }
125
+ }
126
+
127
+
128
+
129
+
130
+
131
+ exports.signWebhook = signWebhook; exports.sendEvent = sendEvent; exports.sendWorkEvent = sendWorkEvent;
132
+ //# sourceMappingURL=chunk-EB5XWQAU.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["/home/runner/work/extension-sdk/extension-sdk/dist/chunk-EB5XWQAU.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-EB5XWQAU.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 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 const normalized = readRecord(event, 'comment');\n const author = readRecord(normalized, 'author');\n return (\n status === undefined &&\n nonempty(normalized.externalCommentId) &&\n typeof normalized.body === 'string' &&\n nonempty(normalized.createdAt) &&\n nonempty(author.id) &&\n nonempty(author.displayName)\n );\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"]}
@@ -1,14 +1,14 @@
1
1
  "use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _nullishCoalesce(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return rhsFn(); } }
2
2
 
3
- var _chunkOPPYEA7Fcjs = require('./chunk-OPPYEA7F.cjs');
3
+ var _chunkPRCZMOLDcjs = require('./chunk-PRCZMOLD.cjs');
4
4
 
5
5
 
6
- var _chunk2DBSVKAUcjs = require('./chunk-2DBSVKAU.cjs');
6
+ var _chunk6HETNZJYcjs = require('./chunk-6HETNZJY.cjs');
7
7
 
8
8
  // src/client/hooks.ts
9
9
  var _react = require('react');
10
10
  function useTapSlice(key, select) {
11
- const tap = _chunkOPPYEA7Fcjs.useTap.call(void 0, );
11
+ const tap = _chunkPRCZMOLDcjs.useTap.call(void 0, );
12
12
  const subscribe = _react.useCallback.call(void 0,
13
13
  (listener) => tap.__subscribe(key, listener),
14
14
  [tap, key]
@@ -45,7 +45,7 @@ function useTapContext() {
45
45
  return useTapSlice("context", selectContext);
46
46
  }
47
47
  function useTapState(key) {
48
- const tap = _chunkOPPYEA7Fcjs.useTap.call(void 0, );
48
+ const tap = _chunkPRCZMOLDcjs.useTap.call(void 0, );
49
49
  const subscribe = _react.useCallback.call(void 0,
50
50
  (listener) => tap.__subscribe(`state:${key}`, listener),
51
51
  [tap, key]
@@ -108,14 +108,14 @@ function useHostQueryClient() {
108
108
  try {
109
109
  return _reactquery.useQueryClient.call(void 0, );
110
110
  } catch (e) {
111
- throw new (0, _chunk2DBSVKAUcjs.TapError)(
111
+ throw new (0, _chunk6HETNZJYcjs.TapError)(
112
112
  "TAP_NO_QUERY_CLIENT",
113
113
  "The Tappify host provides the query client. This hook runs only inside a Tappify mount \u2014 render the component from an entry declared in tappify.extension.json, or wrap it in renderWithTap() from @tappify/extension-sdk/testing."
114
114
  );
115
115
  }
116
116
  }
117
117
  function useTapQuery(query, options = {}) {
118
- const tap = _chunkOPPYEA7Fcjs.useTap.call(void 0, );
118
+ const tap = _chunkPRCZMOLDcjs.useTap.call(void 0, );
119
119
  const client = useHostQueryClient();
120
120
  const filters = useTapFilters();
121
121
  const queryKey = dataQueryKey(tap.extension.installId, query, filters);
@@ -139,7 +139,7 @@ function useTapQuery(query, options = {}) {
139
139
  };
140
140
  }
141
141
  function useTapServer(name, input, options = {}) {
142
- const tap = _chunkOPPYEA7Fcjs.useTap.call(void 0, );
142
+ const tap = _chunkPRCZMOLDcjs.useTap.call(void 0, );
143
143
  const client = useHostQueryClient();
144
144
  const filters = useTapFilters();
145
145
  const queryKey = procedureQueryKey(
@@ -150,7 +150,7 @@ function useTapServer(name, input, options = {}) {
150
150
  );
151
151
  const procedure = tap.server[name];
152
152
  if (typeof procedure !== "function") {
153
- throw new (0, _chunk2DBSVKAUcjs.TapError)(
153
+ throw new (0, _chunk6HETNZJYcjs.TapError)(
154
154
  "TAP_UNKNOWN_PROCEDURE",
155
155
  `Procedure "${name}" is not declared. Add it under server.procedures in tappify.extension.json, then run \`tappify extension types\`.`
156
156
  );
@@ -179,13 +179,13 @@ function isSingletonHandle(value) {
179
179
  }
180
180
  function asSingleton(handle, collection) {
181
181
  if (isSingletonHandle(handle)) return handle;
182
- throw new (0, _chunk2DBSVKAUcjs.TapError)(
182
+ throw new (0, _chunk6HETNZJYcjs.TapError)(
183
183
  "TAP_UNKNOWN_COLLECTION",
184
184
  `useTapStorage("${collection}") reads a singleton collection. Declare it with "singleton": true in tappify.extension.json, or read an id-keyed collection with tap.storage.${collection}.list().`
185
185
  );
186
186
  }
187
187
  function useTapStorage(collection) {
188
- const tap = _chunkOPPYEA7Fcjs.useTap.call(void 0, );
188
+ const tap = _chunkPRCZMOLDcjs.useTap.call(void 0, );
189
189
  const client = useHostQueryClient();
190
190
  const queryKey = storageQueryKey(tap.extension.installId, collection);
191
191
  const handle = asSingleton(tap.storage[collection], collection);
@@ -247,4 +247,4 @@ function useTapStorage(collection) {
247
247
 
248
248
 
249
249
  exports.useTapAuth = useTapAuth; exports.useTapProject = useTapProject; exports.useTapFilters = useTapFilters; exports.useTapTheme = useTapTheme; exports.useTapSize = useTapSize; exports.useTapParams = useTapParams; exports.useTapContext = useTapContext; exports.useTapState = useTapState; exports.dataQueryKey = dataQueryKey; exports.procedureQueryKey = procedureQueryKey; exports.storageQueryKey = storageQueryKey; exports.installPrefix = installPrefix; exports.procedurePrefix = procedurePrefix; exports.useTapQuery = useTapQuery; exports.useTapServer = useTapServer; exports.useTapStorage = useTapStorage;
250
- //# sourceMappingURL=chunk-5AAPD5WV.cjs.map
250
+ //# sourceMappingURL=chunk-GAUZYBXE.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["/home/runner/work/extension-sdk/extension-sdk/dist/chunk-GAUZYBXE.cjs","../src/client/hooks.ts","../src/client/query.ts"],"names":["useCallback"],"mappings":"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","file":"/home/runner/work/extension-sdk/extension-sdk/dist/chunk-GAUZYBXE.cjs","sourcesContent":[null,"import { useCallback, useSyncExternalStore } from 'react';\nimport { useTap } from './context';\nimport type { Tap, TapChangeKey, TapFilters, TapSize } from './types';\n\nfunction useTapSlice<T>(key: TapChangeKey, select: (tap: Tap) => T): T {\n const tap = useTap();\n\n const subscribe = useCallback(\n (listener: () => void) => tap.__subscribe(key, listener),\n [tap, key],\n );\n const snapshot = useCallback(() => select(tap), [tap, select]);\n\n return useSyncExternalStore(subscribe, snapshot, snapshot);\n}\n\nconst selectAuth = (tap: Tap): Tap['auth'] => tap.auth;\nconst selectProject = (tap: Tap): Tap['project'] => tap.project;\nconst selectFilters = (tap: Tap): TapFilters => tap.filters;\nconst selectTheme = (tap: Tap): Tap['theme'] => tap.theme;\nconst selectSize = (tap: Tap): TapSize => tap.ui.size;\nconst selectParams = (tap: Tap): Record<string, string> => tap.params;\nconst selectContext = (tap: Tap): Record<string, unknown> => tap.context;\n\n/**\n * Returns the install token, the signed-in teammate, the workspace id, the\n * granted scopes and `can(scope)`.\n *\n * @remarks\n * Re-renders the component when the host rotates the install token, and when the\n * signed-in teammate's role arrives after the mount. `scopes` and `can` report the\n * grant the mount was built with. Throws `TAP_OUTSIDE_HOST` outside a mount.\n *\n * @example\n * ```tsx\n * import { useTapAuth } from '@tappify/extension-sdk';\n * import { EmptyState } from '@tappify/extension-sdk/ui';\n *\n * function RevenuePanel() {\n * const auth = useTapAuth();\n * if (!auth.can('revenue:read')) {\n * return <EmptyState title=\"Revenue is not shared with this extension\" />;\n * }\n * return <span>Signed in as {auth.user.id}</span>;\n * }\n * ```\n */\nexport function useTapAuth(): Tap['auth'] {\n return useTapSlice('auth', selectAuth);\n}\n\n/**\n * Returns the project the mount renders in, with its apps, releases and tracked\n * keywords.\n *\n * @remarks\n * Re-renders when the host loads or reloads the project. Throws\n * `TAP_OUTSIDE_HOST` outside a mount. An extension installed on the workspace\n * rather than a project still renders inside one project at a time.\n *\n * @example\n * ```tsx\n * import { useTapProject } from '@tappify/extension-sdk';\n *\n * function Platforms() {\n * const project = useTapProject();\n * return <span>{project.name} ships to {project.platforms.join(' and ')}</span>;\n * }\n * ```\n */\nexport function useTapProject(): Tap['project'] {\n return useTapSlice('project', selectProject);\n}\n\n/**\n * Returns the date range, platform and country the owner's filter bar is set to.\n *\n * @remarks\n * Re-renders on every filter change, which is how a surface follows the owner's\n * picker. Pass `range` into a `series`, `reviews`, `crashes` or `revenue` query\n * yourself; the host adds only the country to a data read, while a procedure call\n * carries all three. Throws `TAP_OUTSIDE_HOST` outside a mount.\n *\n * @example\n * ```tsx\n * import { useTapFilters, useTapQuery } from '@tappify/extension-sdk';\n *\n * function Downloads() {\n * const filters = useTapFilters();\n * const downloads = useTapQuery({\n * kind: 'series',\n * metric: 'downloads',\n * range: filters.range,\n * });\n * return <span>{downloads.data?.points.length ?? 0} days</span>;\n * }\n * ```\n */\nexport function useTapFilters(): TapFilters {\n return useTapSlice('filters', selectFilters);\n}\n\n/**\n * Returns the mode the host is drawing in, as `{ mode: 'light' | 'dark' }`.\n *\n * @remarks\n * Re-renders when the owner switches theme. The host's CSS variables already\n * follow the theme, so reach for this only where a component has to branch in\n * code rather than restyle, such as a chart's series colours. Throws\n * `TAP_OUTSIDE_HOST` outside a mount.\n *\n * @example\n * ```tsx\n * import { useTapTheme } from '@tappify/extension-sdk';\n *\n * function SeriesLine() {\n * const theme = useTapTheme();\n * const stroke = theme.mode === 'dark' ? '#a5b4fc' : '#4f46e5';\n * return <svg><line x1=\"0\" y1=\"0\" x2=\"40\" y2=\"10\" stroke={stroke} /></svg>;\n * }\n * ```\n */\nexport function useTapTheme(): Tap['theme'] {\n return useTapSlice('theme', selectTheme);\n}\n\n/**\n * Returns how much room the mount has: `slot`, `panel` or `page`.\n *\n * @remarks\n * Expanding a widget mounts a second copy of the component in the panel rather\n * than resizing the one in the slot, so the change this hook reports is the\n * panel's own Widen and Narrow toggle moving between `panel` and `page`. Size\n * against the mount with a container query where CSS can do the work; this hook is\n * for the cases that need different markup. Throws `TAP_OUTSIDE_HOST` outside a\n * mount.\n *\n * @example\n * ```tsx\n * import { useTapSize } from '@tappify/extension-sdk';\n * import {\n * Table,\n * TableBody,\n * TableCell,\n * TableHead,\n * TableHeader,\n * TableRow,\n * } from '@tappify/extension-sdk/ui';\n *\n * function Keywords({ rows }: { rows: { term: string }[] }) {\n * const size = useTapSize();\n * const shown = size === 'slot' ? rows.slice(0, 3) : rows;\n * return (\n * <Table>\n * <TableHeader>\n * <TableRow variant=\"header\"><TableHead>Term</TableHead></TableRow>\n * </TableHeader>\n * <TableBody>\n * {shown.map(row => (\n * <TableRow key={row.term}><TableCell>{row.term}</TableCell></TableRow>\n * ))}\n * </TableBody>\n * </Table>\n * );\n * }\n * ```\n */\nexport function useTapSize(): TapSize {\n return useTapSlice('size', selectSize);\n}\n\n/**\n * Returns the route of the extension's own page: the page id, the whole sub-path,\n * and one key per segment.\n *\n * @remarks\n * The host builds `pageId`, `path` and then `0`, `1` and so on, one per segment of\n * the sub-path. Only a page entry is routed, so this is empty for a widget, a tab,\n * a row action and a settings panel. Re-renders when the owner navigates inside the\n * page. Throws `TAP_OUTSIDE_HOST` outside a mount.\n *\n * @example\n * ```tsx\n * import { useTapParams } from '@tappify/extension-sdk';\n *\n * function FunnelDetail() {\n * const params = useTapParams();\n * const funnelId = params['1'];\n * return <span>{funnelId ?? params.path}</span>;\n * }\n * ```\n */\nexport function useTapParams(): Record<string, string> {\n return useTapSlice('params', selectParams);\n}\n\n/**\n * Returns what the surface handed the mount: the payload Expand carried in, or\n * the row behind a row action.\n *\n * @remarks\n * Re-renders when the host hands the mount a new context. A row action reads the\n * row from its own `row` prop as well; this hook is the same value for a\n * component further down the tree. Throws `TAP_OUTSIDE_HOST` outside a mount.\n *\n * @example\n * ```tsx\n * import { useTapContext } from '@tappify/extension-sdk';\n *\n * function ContextNote() {\n * const context = useTapContext();\n * const reviewId = context.reviewId;\n * return <span>{typeof reviewId === 'string' ? reviewId : 'No review'}</span>;\n * }\n * ```\n */\nexport function useTapContext(): Record<string, unknown> {\n return useTapSlice('context', selectContext);\n}\n\n/**\n * Returns one value from the in-memory store every mount of the install shares,\n * with a setter for it.\n *\n * @remarks\n * The value is `undefined` until something sets it, and setting it re-renders\n * every mount reading the same key, including a sibling widget and the expand\n * panel. The store lives in the browser session: it is not persisted, not shared\n * between teammates or tabs, and never reaches Tappify or your server — use\n * `useTapStorage` for a value that has to come back tomorrow. Throws\n * `TAP_OUTSIDE_HOST` outside a mount.\n *\n * @example\n * ```tsx\n * import { useTapState } from '@tappify/extension-sdk';\n * import { Button } from '@tappify/extension-sdk/ui';\n *\n * function StepPicker() {\n * const [step, setStep] = useTapState<string>('selectedStep');\n * return (\n * <Button onClick={() => setStep('install')}>\n * {step ?? 'Pick a step'}\n * </Button>\n * );\n * }\n * ```\n */\nexport function useTapState<T>(\n key: string,\n): [T | undefined, (value: T) => void] {\n const tap = useTap();\n\n const subscribe = useCallback(\n (listener: () => void) => tap.__subscribe(`state:${key}`, listener),\n [tap, key],\n );\n const snapshot = useCallback(() => tap.state.get<T>(key), [tap, key]);\n const set = useCallback(\n (value: T) => {\n tap.state.set(key, value);\n },\n [tap, key],\n );\n\n const value = useSyncExternalStore(subscribe, snapshot, snapshot);\n return [value, set];\n}\n","import {\n useQuery,\n useQueryClient,\n type QueryClient,\n} from '@tanstack/react-query';\nimport { useCallback, useEffect, useRef } from 'react';\nimport { useTap } from './context';\nimport { TapError } from './errors';\nimport { useTapFilters } from './hooks';\nimport type {\n Tap,\n TapFilters,\n TapHostEvents,\n TapProcedureInput,\n TapProcedureName,\n TapProcedureOutput,\n TapQuery,\n TapResult,\n TapSingleton,\n TapSingletonDocument,\n TapSingletonName,\n} from './types';\n\nconst STALE_TIME_MS = 30_000;\n\n/** The third argument of `useTapQuery` and `useTapServer`. */\nexport interface TapQueryOptions {\n /** Holds the call back while this is `false`. Defaults to `true`. */\n enabled?: boolean;\n /** Tappify events that mark this result stale and refetch it when they arrive. */\n invalidateOn?: (keyof TapHostEvents)[];\n}\n\n/** What `useTapQuery` and `useTapServer` return. */\nexport interface TapQueryResult<T> {\n /**\n * `undefined` until the first answer for this key arrives. A later call that\n * fails leaves the last answer in place and only sets `error`, so branch on\n * `error` to detect a failure, never on `data`.\n */\n data: T | undefined;\n error: Error | null;\n /** `true` only while the first answer for this key is in flight. */\n isLoading: boolean;\n /** Asks again now, whether or not the cached answer has gone stale. */\n refetch(): void;\n}\n\n/** What `useTapStorage` returns: a read of the document, plus the two writes. */\nexport interface TapStorageResult<T> extends TapQueryResult<T | null> {\n /** Replaces the whole document. */\n save(document: T): Promise<void>;\n /** Merges the fields in and resolves to the merged document. */\n patch(partial: Partial<T>): Promise<T>;\n}\n\n/**\n * Builds the cache key `useTapQuery` reads a Tappify data query under.\n *\n * @remarks\n * The key carries the install, the query and the filters behind it, so two mounts\n * asking for the same numbers share one request. Call it only to reach the query\n * cache directly; the hooks build their own keys.\n *\n * @example\n * ```ts\n * import { dataQueryKey } from '@tappify/extension-sdk';\n *\n * const key = dataQueryKey('ins_test', { kind: 'keywords' }, {\n * range: { from: '2026-09-01', to: '2026-09-08' },\n * platform: 'all',\n * country: 'all',\n * });\n * ```\n */\nexport function dataQueryKey(\n installId: string,\n query: TapQuery,\n filters: TapFilters,\n): unknown[] {\n return ['ext', installId, 'data', query, filters];\n}\n\n/**\n * Builds the cache key `useTapServer` reads one procedure call under.\n *\n * @remarks\n * The input and the filters are part of the key, so the same procedure called\n * with different input caches separately. Call it only to reach the query cache\n * directly.\n *\n * @example\n * ```ts\n * import { procedureQueryKey } from '@tappify/extension-sdk';\n *\n * const key = procedureQueryKey('ins_test', 'getSummary', { days: 7 }, {\n * range: { from: '2026-09-01', to: '2026-09-08' },\n * platform: 'all',\n * country: 'all',\n * });\n * ```\n */\nexport function procedureQueryKey(\n installId: string,\n name: string,\n input: unknown,\n filters: TapFilters,\n): unknown[] {\n return ['ext', installId, 'proc', name, input, filters];\n}\n\n/**\n * Builds the cache key `useTapStorage` reads one collection's document under.\n *\n * @remarks\n * The filters are not part of the key, because a stored document does not depend\n * on them. Call it only to reach the query cache directly.\n *\n * @example\n * ```ts\n * import { storageQueryKey } from '@tappify/extension-sdk';\n *\n * const key = storageQueryKey('ins_test', 'preferences');\n * ```\n */\nexport function storageQueryKey(\n installId: string,\n collection: string,\n): unknown[] {\n return ['ext', installId, 'storage', collection];\n}\n\n/**\n * Builds the key prefix every cached answer of one install sits under.\n *\n * @remarks\n * This is the prefix `tap.invalidate()` with no name invalidates.\n *\n * @example\n * ```ts\n * import { installPrefix } from '@tappify/extension-sdk';\n *\n * const prefix = installPrefix('ins_test');\n * ```\n */\nexport function installPrefix(installId: string): unknown[] {\n return ['ext', installId];\n}\n\n/**\n * Builds the key prefix every cached call of one procedure sits under, whatever\n * its input.\n *\n * @remarks\n * This is the prefix `tap.invalidate(name)` invalidates.\n *\n * @example\n * ```ts\n * import { procedurePrefix } from '@tappify/extension-sdk';\n *\n * const prefix = procedurePrefix('ins_test', 'getSummary');\n * ```\n */\nexport function procedurePrefix(installId: string, name: string): unknown[] {\n return ['ext', installId, 'proc', name];\n}\n\nfunction useInvalidateOn(\n tap: Tap,\n client: QueryClient,\n queryKey: unknown[],\n events: (keyof TapHostEvents)[] | undefined,\n): void {\n const eventsRef = useRef(events);\n const keyRef = useRef(queryKey);\n\n useEffect(() => {\n eventsRef.current = events;\n keyRef.current = queryKey;\n });\n\n const eventSignature = (events ?? []).join(',');\n\n useEffect(() => {\n const names = eventsRef.current ?? [];\n if (names.length === 0) return;\n\n const unsubscribes = names.map(name =>\n tap.data.subscribe(name, () => {\n void client.invalidateQueries({ queryKey: keyRef.current });\n }),\n );\n\n return () => {\n for (const unsubscribe of unsubscribes) unsubscribe();\n };\n }, [tap, client, eventSignature]);\n}\n\nfunction useHostQueryClient(): QueryClient {\n try {\n return useQueryClient();\n } catch {\n throw new TapError(\n 'TAP_NO_QUERY_CLIENT',\n 'The Tappify host provides the query client. This hook runs only inside a Tappify mount — render the component from an entry declared in tappify.extension.json, or wrap it in renderWithTap() from @tappify/extension-sdk/testing.',\n );\n }\n}\n\n/**\n * Reads one query's worth of the owner's Tappify data, cached per install, query\n * and filter bar.\n *\n * @remarks\n * Each kind needs its own scope — `project` needs `projects:read`, `series` needs\n * `analytics:read`, `keywords` and `listing` need `store.metadata:read`, and\n * `reviews`, `crashes` and `revenue` need their own. A kind the install has not\n * granted comes back as a `TapError` with code `TAP_SCOPE_MISSING` in `error`. An\n * answer stays fresh for 30 seconds and a failed read is not retried, so\n * `refetch` is yours to call. The host adds only the country filter to the read:\n * pass `range` into a `series`, `reviews`, `crashes` or `revenue` query yourself,\n * and `platform` into a `series` query, which is the only kind that takes one.\n * Throws `TAP_NO_QUERY_CLIENT` when no Tappify mount provides the query client.\n *\n * @example\n * ```tsx\n * import { useTapFilters, useTapQuery } from '@tappify/extension-sdk';\n * import { Metric } from '@tappify/extension-sdk/ui';\n *\n * function Downloads() {\n * const filters = useTapFilters();\n * const downloads = useTapQuery(\n * { kind: 'series', metric: 'downloads', range: filters.range },\n * { invalidateOn: ['release.shipped'] },\n * );\n * const points = downloads.data?.points ?? [];\n * const total = points.reduce((sum, point) => sum + point.value, 0);\n * return <Metric label=\"Downloads\" value={total} />;\n * }\n * ```\n *\n * @example\n * ```tsx\n * import { useTapQuery } from '@tappify/extension-sdk';\n * import {\n * Table,\n * TableBody,\n * TableCell,\n * TableHead,\n * TableHeader,\n * TableRow,\n * } from '@tappify/extension-sdk/ui';\n *\n * function Keywords() {\n * const keywords = useTapQuery({ kind: 'keywords' });\n * return (\n * <Table>\n * <TableHeader>\n * <TableRow variant=\"header\">\n * <TableHead>Term</TableHead>\n * <TableHead>Rank</TableHead>\n * </TableRow>\n * </TableHeader>\n * <TableBody>\n * {(keywords.data?.keywords ?? []).map(keyword => (\n * <TableRow key={keyword.id}>\n * <TableCell>{keyword.term}</TableCell>\n * <TableCell>{keyword.position}</TableCell>\n * </TableRow>\n * ))}\n * </TableBody>\n * </Table>\n * );\n * }\n * ```\n */\nexport function useTapQuery<Q extends TapQuery>(\n query: Q,\n options: TapQueryOptions = {},\n): TapQueryResult<TapResult<Q>> {\n const tap = useTap();\n const client = useHostQueryClient();\n const filters = useTapFilters();\n const queryKey = dataQueryKey(tap.extension.installId, query, filters);\n\n const result = useQuery({\n queryKey,\n queryFn: () => tap.data.query(query),\n enabled: options.enabled ?? true,\n staleTime: STALE_TIME_MS,\n retry: false,\n });\n\n useInvalidateOn(tap, client, queryKey, options.invalidateOn);\n\n const resultRefetch = result.refetch;\n const refetch = useCallback(() => {\n void resultRefetch();\n }, [resultRefetch]);\n\n return {\n data: result.data,\n error: result.error,\n isLoading: result.isLoading,\n refetch,\n };\n}\n\n/**\n * Calls one procedure on your own server, cached per install, name, input and\n * filter bar.\n *\n * @remarks\n * The name and the input are narrowed by the `TapProcedureMap` augmentation\n * `tappify extension types` writes, so a procedure the manifest does not declare\n * does not compile; at runtime a name the host has no route for throws\n * `TAP_UNKNOWN_PROCEDURE`. The call carries the whole filter bar for you. An\n * answer stays fresh for 30 seconds and a failure is not retried; an error your\n * server threw arrives as a `TapServerError` with your own `code`. Throws\n * `TAP_NO_QUERY_CLIENT` when no Tappify mount provides the query client.\n *\n * @example\n * ```tsx\n * import { useTapFilters, useTapServer } from '@tappify/extension-sdk';\n * import { Button, ErrorState } from '@tappify/extension-sdk/ui';\n *\n * function Summary() {\n * const filters = useTapFilters();\n * const summary = useTapServer(\n * 'getSummary',\n * { days: 7, platform: filters.platform },\n * { invalidateOn: ['settings.changed'] },\n * );\n * if (summary.error !== null) {\n * return <ErrorState title=\"The summary did not load\" description={summary.error.message} />;\n * }\n * return <Button loading={summary.isLoading} onClick={summary.refetch}>Refresh</Button>;\n * }\n * ```\n */\nexport function useTapServer<N extends TapProcedureName>(\n name: N,\n input: TapProcedureInput<N>,\n options: TapQueryOptions = {},\n): TapQueryResult<TapProcedureOutput<N>> {\n const tap = useTap();\n const client = useHostQueryClient();\n const filters = useTapFilters();\n const queryKey = procedureQueryKey(\n tap.extension.installId,\n name,\n input,\n filters,\n );\n const procedure = tap.server[name];\n\n if (typeof procedure !== 'function') {\n throw new TapError(\n 'TAP_UNKNOWN_PROCEDURE',\n `Procedure \"${name}\" is not declared. Add it under server.procedures in tappify.extension.json, then run \\`tappify extension types\\`.`,\n );\n }\n\n const result = useQuery({\n queryKey,\n queryFn: () => procedure(input),\n enabled: options.enabled ?? true,\n staleTime: STALE_TIME_MS,\n retry: false,\n });\n\n useInvalidateOn(tap, client, queryKey, options.invalidateOn);\n\n const resultRefetch = result.refetch;\n const refetch = useCallback(() => {\n void resultRefetch();\n }, [resultRefetch]);\n\n return {\n data: result.data,\n error: result.error,\n isLoading: result.isLoading,\n refetch,\n };\n}\n\nfunction isSingletonHandle<T>(value: unknown): value is TapSingleton<T> {\n return (\n typeof value === 'object' &&\n value !== null &&\n 'get' in value &&\n 'set' in value &&\n 'patch' in value &&\n typeof value.get === 'function' &&\n typeof value.set === 'function' &&\n typeof value.patch === 'function'\n );\n}\n\nfunction asSingleton<T>(handle: unknown, collection: string): TapSingleton<T> {\n if (isSingletonHandle<T>(handle)) return handle;\n\n throw new TapError(\n 'TAP_UNKNOWN_COLLECTION',\n `useTapStorage(\"${collection}\") reads a singleton collection. Declare it with \"singleton\": true in tappify.extension.json, or read an id-keyed collection with tap.storage.${collection}.list().`,\n );\n}\n\n/**\n * Reads and writes the one document a singleton storage collection holds.\n *\n * @remarks\n * The argument is a collection name from the `TapStorageMap` augmentation\n * `tappify extension types` writes, and the collection has to be declared with\n * `\"singleton\": true`; an id-keyed collection throws `TAP_UNKNOWN_COLLECTION`,\n * with the `tap.storage.<name>.list()` path in the message. `data` is `undefined`\n * while the first read is in flight and `null` until the document is first\n * written. Every storage call needs the `storage:write` scope, reads included, and\n * rejects with `TAP_SCOPE_MISSING` without it. A write from another mount or\n * another browser tab of the same install refetches this one.\n *\n * @example\n * ```tsx\n * import { useTapStorage } from '@tappify/extension-sdk';\n * import { Button } from '@tappify/extension-sdk/ui';\n *\n * function CompactToggle() {\n * const preferences = useTapStorage('preferences');\n * return (\n * <Button\n * loading={preferences.isLoading}\n * onClick={() => void preferences.patch({ compact: true })}\n * >\n * Compact\n * </Button>\n * );\n * }\n * ```\n */\nexport function useTapStorage<C extends TapSingletonName>(\n collection: C,\n): TapStorageResult<TapSingletonDocument<C>> {\n type Document = TapSingletonDocument<C>;\n\n const tap = useTap();\n const client = useHostQueryClient();\n const queryKey = storageQueryKey(tap.extension.installId, collection);\n const handle = asSingleton<Document>(tap.storage[collection], collection);\n\n const result = useQuery({\n queryKey,\n queryFn: () => handle.get(),\n staleTime: STALE_TIME_MS,\n retry: false,\n });\n\n const installId = tap.extension.installId;\n const invalidate = useCallback(() => {\n void client.invalidateQueries({\n queryKey: storageQueryKey(installId, collection),\n });\n }, [client, installId, collection]);\n\n useEffect(\n () => tap.__subscribe(`storage:${collection}`, invalidate),\n [tap, collection, invalidate],\n );\n\n const save = useCallback(\n async (document: Document) => {\n await handle.set(document);\n },\n [handle],\n );\n\n const patch = useCallback(\n async (partial: Partial<Document>) => handle.patch(partial),\n [handle],\n );\n\n const resultRefetch = result.refetch;\n const refetch = useCallback(() => {\n void resultRefetch();\n }, [resultRefetch]);\n\n return {\n data: result.data,\n error: result.error,\n isLoading: result.isLoading,\n refetch,\n save,\n patch,\n };\n}\n"]}
@@ -0,0 +1,10 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __export = (target, all) => {
3
+ for (var name in all)
4
+ __defProp(target, name, { get: all[name], enumerable: true });
5
+ };
6
+
7
+ export {
8
+ __export
9
+ };
10
+ //# sourceMappingURL=chunk-J5LGTIGS.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
@@ -1,10 +1,10 @@
1
1
  import {
2
2
  EVENTS
3
- } from "./chunk-MNDVYPEP.mjs";
3
+ } from "./chunk-SGCYEEXD.mjs";
4
4
  import {
5
5
  TapError,
6
6
  TapServerError
7
- } from "./chunk-CSTYBKZC.mjs";
7
+ } from "./chunk-QXD7WVKK.mjs";
8
8
 
9
9
  // src/server/codes.ts
10
10
  var SERVER_ERROR_CODES = {
@@ -124,10 +124,15 @@ function readText(value, key) {
124
124
  // src/server/handler.ts
125
125
  var MAX_BODY_BYTES = 1048576;
126
126
  var WORK_OPERATIONS = [
127
+ "configure",
127
128
  "list_containers",
129
+ "describe_container",
128
130
  "create",
129
131
  "attach",
130
132
  "comment",
133
+ "set_status",
134
+ "hydrate_event",
135
+ "disconnect",
131
136
  "status",
132
137
  "resolve"
133
138
  ];
@@ -238,10 +243,17 @@ function resolveRoute(options, method, segments, url, payload) {
238
243
  }
239
244
  if (head === "work" && method === "POST" && name !== void 0) {
240
245
  const operation = WORK_OPERATIONS.find((candidate) => candidate === name);
241
- return route(
242
- operation === void 0 ? void 0 : options.work?.[operation],
243
- bodyInput
244
- );
246
+ const handler = operation === void 0 ? void 0 : options.work?.[operation];
247
+ if (operation === "set_status" && handler === void 0) {
248
+ const input = readRecord(payload, "input");
249
+ if (readText(input, "tappifyStatus") === "resolved") {
250
+ return route(options.work?.resolve, {
251
+ ...input,
252
+ id: readUnknown(input, "externalItemId")
253
+ });
254
+ }
255
+ }
256
+ return route(handler, bodyInput);
245
257
  }
246
258
  if (head === "events" && method === "POST") {
247
259
  const envelope = readRecord(payload, "event");
@@ -354,12 +366,13 @@ function createTappifyHandler(options) {
354
366
 
355
367
  export {
356
368
  SERVER_ERROR_CODES,
369
+ DEFAULT_JWKS_URL,
370
+ verifyTappifyToken,
357
371
  INTERNAL_MESSAGE,
358
372
  wireError,
373
+ readUnknown,
359
374
  readRecord,
360
375
  readText,
361
- DEFAULT_JWKS_URL,
362
- verifyTappifyToken,
363
376
  createTappifyHandler
364
377
  };
365
- //# sourceMappingURL=chunk-ZT35X7KL.mjs.map
378
+ //# sourceMappingURL=chunk-KZ5NGFUP.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/server/codes.ts","../src/server/token.ts","../src/server/wire.ts","../src/server/handler.ts"],"sourcesContent":["import type { TapErrorCode } from '../client/errors';\n\n/**\n * The codes `createTappifyHandler` answers with itself, keyed by a short name.\n *\n * @remarks\n * Every one of these comes from the SDK, not from your own handlers: a route or\n * handler that is not there, a token that is missing or does not verify, a missing\n * event id, a body that is not JSON or is over 1 MB, a webhook Tappify rejected,\n * and the catch-all for an error a handler let escape. Throw a `TapServerError`\n * with a code of your own for anything your server decides.\n */\nexport const SERVER_ERROR_CODES = {\n ROUTE_UNKNOWN: 'TAP_ROUTE_UNKNOWN',\n HANDLER_MISSING: 'TAP_HANDLER_MISSING',\n INTERNAL_ERROR: 'TAP_INTERNAL_ERROR',\n TOKEN_MISSING: 'TAP_TOKEN_MISSING',\n TOKEN_INVALID: 'TAP_TOKEN_INVALID',\n TOKEN_AUDIENCE: 'TAP_TOKEN_AUDIENCE',\n EVENT_ID_MISSING: 'TAP_EVENT_ID_MISSING',\n BODY_INVALID: 'TAP_BODY_INVALID',\n BODY_TOO_LARGE: 'TAP_BODY_TOO_LARGE',\n WEBHOOK_REJECTED: 'TAP_WEBHOOK_REJECTED',\n} as const satisfies Record<string, TapErrorCode>;\n\n/** Every code the handler answers with itself, as a union of its own values. */\nexport type TapServerErrorCode =\n (typeof SERVER_ERROR_CODES)[keyof typeof SERVER_ERROR_CODES];\n","import {\n createLocalJWKSet,\n createRemoteJWKSet,\n jwtVerify,\n type JWTPayload,\n type JWTVerifyGetKey,\n} from 'jose';\nimport { TapError } from '../client/errors';\nimport type { TappifyClaims, VerifyTokenOptions } from './types';\n\n/**\n * The key set `verifyTappifyToken` fetches when no `jwksUrl` or `jwks` is given.\n *\n * @remarks\n * This is production. Point `jwksUrl` at the environment that is calling you when\n * you run against another one, and pass `jwks` in tests.\n */\nexport const DEFAULT_JWKS_URL =\n 'https://api.tappify.ai/api/v1/extensions/.well-known/jwks.json';\n\nconst CLOCK_TOLERANCE_SECONDS = 60;\n\nconst remoteSets = new Map<string, JWTVerifyGetKey>();\n\nfunction keySet(options: VerifyTokenOptions): JWTVerifyGetKey {\n if (options.jwks) return createLocalJWKSet(options.jwks);\n\n const url = options.jwksUrl ?? DEFAULT_JWKS_URL;\n const cached = remoteSets.get(url);\n if (cached) return cached;\n\n const created = createRemoteJWKSet(new URL(url));\n remoteSets.set(url, created);\n return created;\n}\n\nfunction readString(payload: JWTPayload, key: string): string | undefined {\n const value = payload[key];\n return typeof value === 'string' ? value : undefined;\n}\n\nfunction toClaims(payload: JWTPayload, audience: string): TappifyClaims {\n const aud = payload.aud;\n const matches =\n aud === audience || (Array.isArray(aud) && aud.includes(audience));\n\n if (!matches) {\n throw new TapError(\n 'TAP_TOKEN_AUDIENCE',\n `This install token was issued for a different extension. Create the handler with the same extensionId as the \"id\" in tappify.extension.json, so its audience is \"${audience}\".`,\n );\n }\n\n const installId = readString(payload, 'installId');\n const extensionId = readString(payload, 'extensionId');\n const organizationId = readString(payload, 'organizationId');\n\n if (\n installId === undefined ||\n extensionId === undefined ||\n organizationId === undefined\n ) {\n throw new TapError(\n 'TAP_TOKEN_INVALID',\n `The install token is missing the installId, extensionId or organizationId claim. Verify against ${DEFAULT_JWKS_URL} and do not reuse tokens across environments.`,\n );\n }\n\n const scopes = payload.scopes;\n const userId = readString(payload, 'userId');\n\n const claims: TappifyClaims = {\n iss: readString(payload, 'iss') ?? '',\n aud: audience,\n exp: typeof payload.exp === 'number' ? payload.exp : 0,\n iat: typeof payload.iat === 'number' ? payload.iat : 0,\n installId,\n extensionId,\n projectId: readString(payload, 'projectId') ?? null,\n organizationId,\n scopes: Array.isArray(scopes)\n ? scopes.filter((scope): scope is string => typeof scope === 'string')\n : [],\n };\n\n if (userId !== undefined) claims.userId = userId;\n return claims;\n}\n\n/**\n * Verifies an install token against Tappify's signing keys and returns its\n * claims.\n *\n * @remarks\n * Checks the RS256 signature, the expiry with 60 seconds of clock tolerance, and\n * that the audience is `ext:<extensionId>`. Rejects with a `TapError` carrying\n * `TAP_TOKEN_INVALID` when the signature, the expiry or a required claim is wrong,\n * and `TAP_TOKEN_AUDIENCE` when the token was issued for another extension. A\n * remote key set is fetched once per url and cached for the process, so calling\n * this per request costs nothing after the first. `createTappifyHandler` already\n * verifies every route but health, so call this only outside the handler, or\n * where you accept a Tappify token on a route of your own.\n *\n * @example\n * ```ts\n * import { verifyTappifyToken } from '@tappify/extension-sdk/server';\n *\n * async function whoCalled(request: Request): Promise<string> {\n * const token = (request.headers.get('authorization') ?? '').replace('Bearer ', '');\n * const claims = await verifyTappifyToken(token, { extensionId: 'starter' });\n * return claims.installId;\n * }\n * ```\n */\nexport async function verifyTappifyToken(\n token: string,\n options: VerifyTokenOptions,\n): Promise<TappifyClaims> {\n const audience = `ext:${options.extensionId}`;\n\n let payload: JWTPayload;\n try {\n const verified = await jwtVerify(token, keySet(options), {\n algorithms: ['RS256'],\n clockTolerance: CLOCK_TOLERANCE_SECONDS,\n });\n payload = verified.payload;\n } catch {\n throw new TapError(\n 'TAP_TOKEN_INVALID',\n 'The install token did not verify. Check that your server reads the Authorization header unchanged, that its clock is correct, and that jwksUrl points at the Tappify environment that called you.',\n );\n }\n\n return toClaims(payload, audience);\n}\n","import type { TapServerErrorCode } from './codes';\n\nexport const INTERNAL_MESSAGE =\n 'The extension server failed to handle this call. Check your own logs; Tappify records the failure against your extension.';\n\nexport function json(data: unknown, status = 200): Response {\n return new Response(JSON.stringify(data), {\n status,\n headers: { 'content-type': 'application/json' },\n });\n}\n\nexport function wireError(\n code: string,\n message: string,\n status: number,\n): Response {\n return json({ error: { code, message } }, status);\n}\n\nexport function failure(\n code: TapServerErrorCode,\n message: string,\n status: number,\n): Response {\n return wireError(code, message, status);\n}\n\nexport function readUnknown(value: unknown, key: string): unknown {\n if (typeof value !== 'object' || value === null) return undefined;\n return (value as Record<string, unknown>)[key];\n}\n\nexport function readRecord(\n value: unknown,\n key: string,\n): Record<string, unknown> {\n const nested = readUnknown(value, key);\n return typeof nested === 'object' && nested !== null\n ? (nested as Record<string, unknown>)\n : {};\n}\n\nexport function readText(value: unknown, key: string): string | null {\n const nested = readUnknown(value, key);\n return typeof nested === 'string' ? nested : null;\n}\n","import { TapError, TapServerError } from '../client/errors';\nimport type { TapCredentialValues, TapDateRange } from '../client/types';\nimport { EVENTS } from '../manifest/constants';\nimport { SERVER_ERROR_CODES, type TapServerErrorCode } from './codes';\nimport { verifyTappifyToken } from './token';\nimport type {\n ContextBlock,\n LegacyWorkHandlers,\n MentionItems,\n MetricsInput,\n MetricsResponse,\n TappifyCallContext,\n TappifyClaims,\n TappifyDocuments,\n TappifyHandlerOptions,\n TappifyHealth,\n TappifyInstall,\n TappifyRequest,\n WorkOperation,\n} from './types';\nimport {\n INTERNAL_MESSAGE,\n failure,\n json,\n readRecord,\n readText,\n readUnknown,\n wireError,\n} from './wire';\n\n/**\n * What `createTappifyHandler` returns: a `fetch` handler, which is what Cloudflare\n * Workers, Vercel and the two adapters all take.\n */\nexport type TappifyFetchHandler = (request: Request) => Promise<Response>;\n\nconst MAX_BODY_BYTES = 1_048_576;\n\nconst WORK_OPERATIONS: readonly (WorkOperation | keyof LegacyWorkHandlers)[] = [\n 'configure',\n 'list_containers',\n 'describe_container',\n 'create',\n 'attach',\n 'comment',\n 'set_status',\n 'hydrate_event',\n 'disconnect',\n 'status',\n 'resolve',\n];\n\nconst TOKEN_FAILURES: readonly TapServerErrorCode[] = [\n SERVER_ERROR_CODES.TOKEN_INVALID,\n SERVER_ERROR_CODES.TOKEN_AUDIENCE,\n];\n\n/**\n * The token is signed by Tappify and the body is not, so the claims win wherever\n * they carry a value and the body only fills what the token leaves out.\n */\nfunction toInstall(payload: unknown, claims: TappifyClaims): TappifyInstall {\n const raw = readRecord(payload, 'install');\n return {\n id: claims.installId,\n extensionId: claims.extensionId,\n projectId: claims.projectId ?? readText(raw, 'projectId'),\n organizationId: claims.organizationId,\n };\n}\n\nfunction toPlatform(value: string | null): TappifyCallContext['platform'] {\n return value === 'ios' || value === 'android' ? value : 'all';\n}\n\nfunction toPreset(value: string | null): TapDateRange['preset'] {\n return value === '7d' || value === '30d' || value === '90d'\n ? value\n : undefined;\n}\n\nfunction toContext(payload: unknown): TappifyCallContext {\n const raw = readRecord(payload, 'context');\n const filters = readRecord(raw, 'filters');\n const rawRange = readRecord(filters, 'range');\n const from = readText(raw, 'from') ?? readText(rawRange, 'from') ?? '';\n const to = readText(raw, 'to') ?? readText(rawRange, 'to') ?? '';\n\n const range: TapDateRange = {\n from: readText(rawRange, 'from') ?? from,\n to: readText(rawRange, 'to') ?? to,\n };\n\n const preset = toPreset(readText(rawRange, 'preset'));\n if (preset !== undefined) range.preset = preset;\n\n return {\n projectId: readText(raw, 'projectId'),\n platform: toPlatform(readText(raw, 'platform')),\n from,\n to,\n filters: {\n range,\n platform: toPlatform(readText(filters, 'platform')),\n country: readText(filters, 'country') ?? 'all',\n },\n };\n}\n\nfunction toCredentials(payload: unknown): TapCredentialValues {\n const raw = readRecord(payload, 'credentials');\n const credentials: Record<string, string> = {};\n for (const [name, value] of Object.entries(raw)) {\n if (typeof value === 'string') credentials[name] = value;\n }\n return credentials;\n}\n\nfunction tooLarge(): TapServerError {\n return new TapServerError(\n SERVER_ERROR_CODES.BODY_TOO_LARGE,\n 'A Tappify request body is at most 1 MB. Return a cursor or an id the host can follow instead of inlining the payload.',\n 413,\n );\n}\n\nasync function readBody(request: Request): Promise<unknown> {\n if (request.method === 'GET' || request.method === 'HEAD') return {};\n\n const length = request.headers.get('content-length');\n if (length !== null && Number(length) > MAX_BODY_BYTES) throw tooLarge();\n\n const bytes = await request.arrayBuffer();\n if (bytes.byteLength > MAX_BODY_BYTES) throw tooLarge();\n if (bytes.byteLength === 0) return {};\n\n const text = new TextDecoder().decode(bytes);\n try {\n return JSON.parse(text);\n } catch {\n throw new TapServerError(\n SERVER_ERROR_CODES.BODY_INVALID,\n 'The request body was not JSON. Do not parse or re-encode the body before handing the Request to createTappifyHandler().',\n 400,\n );\n }\n}\n\n/**\n * The route table holds handlers whose declared input and event types the\n * manifest augmentations narrow. Method syntax keeps the parameter bivariant,\n * so a narrowed handler still fits a table the wire types as `unknown`.\n */\ntype AnyRouteHandler = {\n route(request: TappifyRequest<unknown, unknown>): unknown;\n}['route'];\n\ninterface Route {\n handler: AnyRouteHandler | undefined;\n input: unknown;\n event: unknown;\n noContent: boolean;\n}\n\nfunction route(\n handler: AnyRouteHandler | undefined,\n input: unknown,\n event?: unknown,\n noContent = false,\n): Route {\n return { handler, input, event, noContent };\n}\n\nfunction handlerFor(\n map: Partial<Record<string, AnyRouteHandler>> | undefined,\n name: string,\n): AnyRouteHandler | undefined {\n return map?.[name];\n}\n\nfunction resolveRoute(\n options: TappifyHandlerOptions,\n method: string,\n segments: string[],\n url: URL,\n payload: unknown,\n): Route | null {\n const [head, name] = segments;\n const bodyInput = readUnknown(payload, 'input') ?? {};\n\n if (head === 'metrics' && method === 'POST') {\n return route(options.metrics, bodyInput);\n }\n if (head === 'tools' && method === 'POST' && name !== undefined) {\n return route(handlerFor(options.tools, name), bodyInput);\n }\n if (head === 'mentions' && method === 'GET' && name !== undefined) {\n return route(handlerFor(options.mentions, name), {\n q: url.searchParams.get('q') ?? '',\n });\n }\n if (head === 'actions' && method === 'POST' && name !== undefined) {\n return route(handlerFor(options.actions, name), bodyInput);\n }\n if (head === 'procedures' && method === 'POST' && name !== undefined) {\n return route(handlerFor(options.procedures, name), bodyInput);\n }\n if (head === 'context' && method === 'GET' && name !== undefined) {\n return route(handlerFor(options.context, name), undefined);\n }\n if (head === 'work' && method === 'POST' && name !== undefined) {\n const operation = WORK_OPERATIONS.find(candidate => candidate === name);\n const handler =\n operation === undefined ? undefined : options.work?.[operation];\n if (operation === 'set_status' && handler === undefined) {\n const input = readRecord(payload, 'input');\n if (readText(input, 'tappifyStatus') === 'resolved') {\n // SDK v2 resolve handlers conventionally read the provider item as id.\n // eslint-disable-next-line @typescript-eslint/no-deprecated -- SDK v2 compatibility adapter.\n return route(options.work?.resolve, {\n ...input,\n id: readUnknown(input, 'externalItemId'),\n });\n }\n }\n return route(handler, bodyInput);\n }\n if (head === 'events' && method === 'POST') {\n const envelope = readRecord(payload, 'event');\n const eventName = readText(envelope, 'name');\n if (eventName === null) {\n throw new TapServerError(\n SERVER_ERROR_CODES.BODY_INVALID,\n 'The events body has no event.name. Post the envelope Tappify sends unchanged, with the event name under \"event\".',\n 400,\n );\n }\n const declared = EVENTS.find(candidate => candidate === eventName);\n return route(\n declared === undefined ? undefined : options.events?.[declared],\n undefined,\n readUnknown(envelope, 'payload') ?? {},\n true,\n );\n }\n\n return null;\n}\n\n/**\n * Builds the one `fetch` handler that answers every route Tappify calls on your\n * server.\n *\n * @remarks\n * It routes the paths under `/tappify`, minus `basePath` when your framework mounts\n * it under a prefix, and hands each matched handler a `TappifyRequest`. Every\n * route but `GET /tappify/health` needs a bearer install token, which it verifies\n * with `verifyTappifyToken`, and an `X-Tappify-Event-Id` header; a missing token is\n * 401 `TAP_TOKEN_MISSING`, a missing id 400 `TAP_EVENT_ID_MISSING`, an unknown path\n * 404 `TAP_ROUTE_UNKNOWN` and a declared name with no handler 404\n * `TAP_HANDLER_MISSING`. A body that is not JSON is 400 `TAP_BODY_INVALID` and one\n * over 1 MB is 413 `TAP_BODY_TOO_LARGE`. A `TapServerError` a handler throws is\n * answered with its own code and status, and a `TapError` other than the two token\n * failures with its own code and 400; anything else becomes 500\n * `TAP_INTERNAL_ERROR` with a fixed message, so an internal failure never reaches\n * the owner. An events route answers 204 with no body, and every other route\n * answers the handler's return value as JSON. Export the options object as well as\n * the handler: `createTestClient` takes the options.\n *\n * @example\n * ```ts\n * import {\n * createTappifyHandler,\n * type TappifyHandlerOptions,\n * } from '@tappify/extension-sdk/server';\n *\n * const handlerOptions: TappifyHandlerOptions = {\n * extensionId: 'starter',\n * health: () => ({ ok: true, version: '0.1.0' }),\n * procedures: {\n * getSummary: () => ({ installs: 1260, delta: 0.087, topKeyword: 'photo editor' }),\n * },\n * };\n *\n * const handler = createTappifyHandler(handlerOptions);\n * ```\n */\nexport function createTappifyHandler(\n options: TappifyHandlerOptions,\n): TappifyFetchHandler {\n const basePath = options.basePath ?? '';\n\n return async function handle(request: Request): Promise<Response> {\n const url = new URL(request.url);\n const path = url.pathname.startsWith(basePath)\n ? url.pathname.slice(basePath.length)\n : url.pathname;\n\n const segments = path.split('/').filter(segment => segment !== '');\n if (segments[0] !== 'tappify') {\n return failure(\n SERVER_ERROR_CODES.ROUTE_UNKNOWN,\n `Nothing is mounted at ${path}. Tappify calls paths under /tappify; set basePath if your framework mounts the handler under a prefix.`,\n 404,\n );\n }\n\n const rest = segments.slice(1);\n\n if (rest[0] === 'health' && request.method === 'GET') {\n const health: TappifyHealth = options.health\n ? await options.health()\n : { ok: true };\n return json(health);\n }\n\n const authorization = request.headers.get('authorization') ?? '';\n const token = authorization.startsWith('Bearer ')\n ? authorization.slice('Bearer '.length)\n : '';\n\n if (token === '') {\n return failure(\n SERVER_ERROR_CODES.TOKEN_MISSING,\n 'This route needs an install token. Tappify sends it as Authorization: Bearer <token>; forward the header unchanged from your framework.',\n 401,\n );\n }\n\n const eventId = request.headers.get('x-tappify-event-id');\n if (eventId === null || eventId === '') {\n return failure(\n SERVER_ERROR_CODES.EVENT_ID_MISSING,\n 'Every Tappify call carries X-Tappify-Event-Id for idempotency. Forward the header unchanged, and use it as the key when you deduplicate.',\n 400,\n );\n }\n\n try {\n const claims = await verifyTappifyToken(token, {\n extensionId: options.extensionId,\n jwksUrl: options.jwksUrl,\n jwks: options.jwks,\n });\n\n const payload = await readBody(request);\n const matched = resolveRoute(options, request.method, rest, url, payload);\n\n if (matched === null) {\n return failure(\n SERVER_ERROR_CODES.ROUTE_UNKNOWN,\n `Tappify has no ${request.method} ${path} route. The paths it calls are listed under \"Vendor server endpoints\" in the reference; check the method and the spelling.`,\n 404,\n );\n }\n\n if (matched.handler === undefined) {\n return failure(\n SERVER_ERROR_CODES.HANDLER_MISSING,\n `No handler is registered for ${request.method} ${path}. Add it to createTappifyHandler(), or remove the contribution from tappify.extension.json.`,\n 404,\n );\n }\n\n const documents = readRecord(payload, 'documents');\n\n const typed: TappifyRequest<unknown, unknown> = {\n install: toInstall(payload, claims),\n context: toContext(payload),\n input: matched.input,\n credentials: toCredentials(payload),\n documents: documents as TappifyDocuments,\n event: matched.event,\n eventId,\n claims,\n request,\n };\n\n const result = await matched.handler(typed);\n return matched.noContent\n ? new Response(null, { status: 204 })\n : json(result);\n } catch (error) {\n if (TapServerError.is(error)) {\n return wireError(error.code, error.message, error.status ?? 400);\n }\n if (TapError.is(error)) {\n const tokenFailure = TOKEN_FAILURES.find(\n candidate => candidate === error.code,\n );\n if (tokenFailure !== undefined) {\n return failure(tokenFailure, error.message, 401);\n }\n return wireError(error.code, error.message, 400);\n }\n return failure(SERVER_ERROR_CODES.INTERNAL_ERROR, INTERNAL_MESSAGE, 500);\n }\n };\n}\n\nexport type {\n ContextBlock,\n MentionItems,\n MetricsInput,\n MetricsResponse,\n TappifyClaims,\n};\n"],"mappings":"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","names":[]}