@tappify/extension-sdk 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (98) hide show
  1. package/LICENSE +21 -0
  2. package/dist/chunk-2DBSVKAU.cjs +65 -0
  3. package/dist/chunk-2DBSVKAU.cjs.map +1 -0
  4. package/dist/chunk-3YWVYA42.cjs +142 -0
  5. package/dist/chunk-3YWVYA42.cjs.map +1 -0
  6. package/dist/chunk-4C5Z3CIK.mjs +26 -0
  7. package/dist/chunk-4C5Z3CIK.mjs.map +1 -0
  8. package/dist/chunk-5AAPD5WV.cjs +250 -0
  9. package/dist/chunk-5AAPD5WV.cjs.map +1 -0
  10. package/dist/chunk-7KIKOH5H.cjs +26 -0
  11. package/dist/chunk-7KIKOH5H.cjs.map +1 -0
  12. package/dist/chunk-BDCX6LRQ.mjs +250 -0
  13. package/dist/chunk-BDCX6LRQ.mjs.map +1 -0
  14. package/dist/chunk-CSTYBKZC.mjs +65 -0
  15. package/dist/chunk-CSTYBKZC.mjs.map +1 -0
  16. package/dist/chunk-IGK2AP6J.mjs +142 -0
  17. package/dist/chunk-IGK2AP6J.mjs.map +1 -0
  18. package/dist/chunk-MNDVYPEP.mjs +302 -0
  19. package/dist/chunk-MNDVYPEP.mjs.map +1 -0
  20. package/dist/chunk-OPPYEA7F.cjs +33 -0
  21. package/dist/chunk-OPPYEA7F.cjs.map +1 -0
  22. package/dist/chunk-PYJMJF44.cjs +302 -0
  23. package/dist/chunk-PYJMJF44.cjs.map +1 -0
  24. package/dist/chunk-QHM7XNRK.mjs +33 -0
  25. package/dist/chunk-QHM7XNRK.mjs.map +1 -0
  26. package/dist/chunk-RMUWRESK.cjs +365 -0
  27. package/dist/chunk-RMUWRESK.cjs.map +1 -0
  28. package/dist/chunk-ZT35X7KL.mjs +365 -0
  29. package/dist/chunk-ZT35X7KL.mjs.map +1 -0
  30. package/dist/codegen.cjs +300 -0
  31. package/dist/codegen.cjs.map +1 -0
  32. package/dist/codegen.d.cts +432 -0
  33. package/dist/codegen.d.ts +432 -0
  34. package/dist/codegen.mjs +272 -0
  35. package/dist/codegen.mjs.map +1 -0
  36. package/dist/constants-DJ_KgZpN.d.cts +449 -0
  37. package/dist/constants-DJ_KgZpN.d.ts +449 -0
  38. package/dist/context-Dt6xh_5t.d.cts +30 -0
  39. package/dist/context-Ml7k_V14.d.ts +30 -0
  40. package/dist/errors-COsBh7kd.d.cts +88 -0
  41. package/dist/errors-COsBh7kd.d.ts +88 -0
  42. package/dist/handler-CZIlZ5vu.d.ts +227 -0
  43. package/dist/handler-kU9Jf36K.d.cts +227 -0
  44. package/dist/host.cjs +24 -0
  45. package/dist/host.cjs.map +1 -0
  46. package/dist/host.d.cts +24 -0
  47. package/dist/host.d.ts +24 -0
  48. package/dist/host.mjs +24 -0
  49. package/dist/host.mjs.map +1 -0
  50. package/dist/index.cjs +698 -0
  51. package/dist/index.cjs.map +1 -0
  52. package/dist/index.d.cts +1016 -0
  53. package/dist/index.d.ts +1016 -0
  54. package/dist/index.mjs +698 -0
  55. package/dist/index.mjs.map +1 -0
  56. package/dist/manifest.cjs +872 -0
  57. package/dist/manifest.cjs.map +1 -0
  58. package/dist/manifest.d.cts +432 -0
  59. package/dist/manifest.d.ts +432 -0
  60. package/dist/manifest.mjs +872 -0
  61. package/dist/manifest.mjs.map +1 -0
  62. package/dist/matchers-Cz93AK1n.d.cts +74 -0
  63. package/dist/matchers-Cz93AK1n.d.ts +74 -0
  64. package/dist/server.cjs +210 -0
  65. package/dist/server.cjs.map +1 -0
  66. package/dist/server.d.cts +243 -0
  67. package/dist/server.d.ts +243 -0
  68. package/dist/server.mjs +210 -0
  69. package/dist/server.mjs.map +1 -0
  70. package/dist/styles.css +367 -0
  71. package/dist/testing/index.cjs +887 -0
  72. package/dist/testing/index.cjs.map +1 -0
  73. package/dist/testing/index.d.cts +512 -0
  74. package/dist/testing/index.d.ts +512 -0
  75. package/dist/testing/index.mjs +887 -0
  76. package/dist/testing/index.mjs.map +1 -0
  77. package/dist/testing/jest.cjs +17 -0
  78. package/dist/testing/jest.cjs.map +1 -0
  79. package/dist/testing/jest.d.cts +12 -0
  80. package/dist/testing/jest.d.ts +12 -0
  81. package/dist/testing/jest.mjs +17 -0
  82. package/dist/testing/jest.mjs.map +1 -0
  83. package/dist/testing/vitest.cjs +15 -0
  84. package/dist/testing/vitest.cjs.map +1 -0
  85. package/dist/testing/vitest.d.cts +10 -0
  86. package/dist/testing/vitest.d.ts +10 -0
  87. package/dist/testing/vitest.mjs +15 -0
  88. package/dist/testing/vitest.mjs.map +1 -0
  89. package/dist/types-DDAGLpov.d.ts +715 -0
  90. package/dist/types-DrpQSv_2.d.cts +715 -0
  91. package/dist/vite.cjs +1344 -0
  92. package/dist/vite.cjs.map +1 -0
  93. package/dist/vite.d.cts +434 -0
  94. package/dist/vite.d.ts +434 -0
  95. package/dist/vite.mjs +1314 -0
  96. package/dist/vite.mjs.map +1 -0
  97. package/package.json +268 -0
  98. package/schema/extension-v2.json +1500 -0
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["/Users/vulakh/Desktop/Work/Tappify/extension-sdk/dist/chunk-PYJMJF44.cjs","../src/manifest/constants.ts"],"names":[],"mappings":"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","file":"/Users/vulakh/Desktop/Work/Tappify/extension-sdk/dist/chunk-PYJMJF44.cjs","sourcesContent":[null,"import type { ToolDeclaration } from './types';\n\nexport const SCOPES = [\n {\n key: 'ui:render',\n label: 'Render UI',\n description:\n 'Render the declared pages, tabs, widgets, row actions and settings panel, and open chat with a visible prompt',\n requiresSecurityReview: false,\n requiresJustification: false,\n },\n {\n key: 'projects:read',\n label: 'Read the project',\n description: 'Project name, platforms and store identity',\n requiresSecurityReview: false,\n requiresJustification: false,\n },\n {\n key: 'analytics:read',\n label: 'Read analytics',\n description: 'Downloads, impressions, page views and conversion series',\n requiresSecurityReview: false,\n requiresJustification: false,\n },\n {\n key: 'store.metadata:read',\n label: 'Read store metadata',\n description: 'Listing metadata, keyword set and keyword positions',\n requiresSecurityReview: false,\n requiresJustification: false,\n },\n {\n key: 'reviews:read',\n label: 'Read store reviews',\n description: 'Review text, ratings and replies',\n requiresSecurityReview: false,\n requiresJustification: false,\n },\n {\n key: 'crashes:read',\n label: 'Read crash data',\n description: 'Crash-free rate and issue counts',\n requiresSecurityReview: false,\n requiresJustification: false,\n },\n {\n key: 'revenue:read',\n label: 'Read revenue',\n description: 'MRR, revenue series and refund counts',\n requiresSecurityReview: false,\n requiresJustification: true,\n },\n {\n key: 'metrics:write',\n label: 'Write connector metrics',\n description:\n 'Write the metrics this extension declares into the metrics store',\n requiresSecurityReview: false,\n requiresJustification: false,\n },\n {\n key: 'alerts:write',\n label: 'Raise alerts and banners',\n description:\n 'Turn declared webhooks into alerts and banners, at most three a day per project',\n requiresSecurityReview: false,\n requiresJustification: true,\n },\n {\n key: 'insights:write',\n label: 'Post insights',\n description: 'Post insights to Home and to chat',\n requiresSecurityReview: false,\n requiresJustification: true,\n },\n {\n key: 'ai:tools',\n label: 'Add assistant tools',\n description: 'Offer tools and mentions to the assistant',\n requiresSecurityReview: false,\n requiresJustification: false,\n },\n {\n key: 'ai:actions',\n label: 'Run assistant actions',\n description: 'Run declared actions, each behind the approval card',\n requiresSecurityReview: true,\n requiresJustification: true,\n },\n {\n key: 'store.metadata:write',\n label: 'Propose store metadata changes',\n description: 'Propose listing changes through a Tappify approval',\n requiresSecurityReview: true,\n requiresJustification: true,\n },\n {\n key: 'autopilot:trigger',\n label: 'Trigger automated store actions',\n description: 'Start automated store actions on the owner’s behalf',\n requiresSecurityReview: true,\n requiresJustification: true,\n },\n {\n key: 'work:create',\n label: 'Create work items',\n description: 'Create issues, pages or messages in the vendor’s system',\n requiresSecurityReview: false,\n requiresJustification: true,\n },\n {\n key: 'work:sync',\n label: 'Sync work items',\n description: 'Two-way comments and status on created work items',\n requiresSecurityReview: false,\n requiresJustification: true,\n },\n {\n key: 'storage:write',\n label: 'Use hosted storage',\n description:\n 'Read and write this extension’s own hosted storage collections',\n requiresSecurityReview: false,\n requiresJustification: false,\n },\n {\n key: 'messaging:send',\n label: 'Send messages to end users',\n description:\n 'Send messages or push notifications to the owner’s end users through owner-supplied credentials',\n requiresSecurityReview: true,\n requiresJustification: true,\n },\n {\n key: 'ai:skills',\n label: 'Shape the assistant',\n description:\n 'Add skills, prompt templates and context providers that shape the assistant’s behaviour',\n requiresSecurityReview: false,\n requiresJustification: true,\n },\n] as const;\n\nexport type ScopeDefinition = (typeof SCOPES)[number];\n/** Every scope an extension can ask for under `scopes` in its manifest. */\nexport type ScopeKey = ScopeDefinition['key'];\n\nexport const SCOPE_KEYS: readonly ScopeKey[] = SCOPES.map(scope => scope.key);\n\nexport const SLOTS = [\n 'kpi-row',\n 'insights',\n 'sidebar',\n 'below-chart',\n 'below-list',\n] as const;\n/** The anchors on a host page a widget can be declared at. */\nexport type SlotId = (typeof SLOTS)[number];\n\nexport const CHARTS = [\n 'installs-activity',\n 'conversion',\n 'release-cadence',\n] as const;\nexport type HostChart = (typeof CHARTS)[number];\n\nexport const PAGES = [\n {\n key: 'overview',\n label: 'Home',\n route: '/projects/:id/overview',\n slots: ['kpi-row', 'insights', 'sidebar'],\n charts: ['installs-activity'],\n tabs: true,\n banners: true,\n },\n {\n key: 'analytics',\n label: 'Analytics',\n route: '/projects/:id/analytics',\n slots: ['kpi-row', 'below-chart', 'sidebar'],\n charts: ['installs-activity', 'conversion'],\n tabs: true,\n banners: true,\n },\n {\n key: 'deployments',\n label: 'Deployments',\n route: '/projects/:id/deployments',\n slots: ['sidebar', 'below-list'],\n charts: ['release-cadence'],\n tabs: true,\n banners: false,\n },\n {\n key: 'ai_chat',\n label: 'Assistant',\n route: 'chat',\n slots: [],\n charts: [],\n tabs: false,\n banners: false,\n },\n] as const;\n\nexport type PageDefinition = (typeof PAGES)[number];\n/** The host pages a contribution can attach a tab, widget or banner to. */\nexport type HostPage = PageDefinition['key'];\n\nexport const PAGE_KEYS: readonly HostPage[] = PAGES.map(page => page.key);\n\nexport const TABLES = [\n { key: 'analytics.keywords', label: 'Keywords' },\n { key: 'analytics.reviews', label: 'Reviews' },\n { key: 'deployments.releases', label: 'Releases' },\n { key: 'deployments.builds', label: 'Builds' },\n] as const;\n\nexport type TableDefinition = (typeof TABLES)[number];\nexport type HostTable = TableDefinition['key'];\n\nexport const TABLE_KEYS: readonly HostTable[] = TABLES.map(table => table.key);\n\nexport const KINDS = [\n 'page',\n 'tab',\n 'widget',\n 'series',\n 'banner',\n 'rowAction',\n 'marker',\n 'settings',\n 'storage',\n 'connector',\n 'webhook',\n 'tool',\n 'mention',\n 'action',\n 'knowledge',\n 'skill',\n 'prompt',\n 'context',\n 'work',\n] as const;\nexport type ContributionKind = (typeof KINDS)[number];\n\nexport const CATEGORIES = [\n 'market_data',\n 'subscriptions',\n 'product_analytics',\n 'crashes',\n 'attribution',\n 'experiments',\n 'messaging',\n 'session_replay',\n 'localization',\n 'design',\n 'ci_cd',\n 'trackers',\n 'support',\n 'chat',\n 'ad_monetization',\n 'warehouse',\n 'compliance',\n 'additional_stores',\n] as const;\nexport type ExtensionCategory = (typeof CATEGORIES)[number];\n\nexport const EVENTS = [\n 'release.shipped',\n 'metadata.changed',\n 'keyword.set_changed',\n 'screenshots.updated',\n 'price.changed',\n 'featuring.started',\n 'score.changed',\n 'incident.opened',\n 'incident.resolved',\n 'playbook.step_approved',\n 'review.thread_opened',\n 'review.thread_resolved',\n 'approval.requested',\n 'digest.sent',\n 'install.created',\n 'install.paused',\n 'install.resumed',\n 'install.revoked',\n 'install.token_rotated',\n 'scopes.changed',\n 'settings.changed',\n] as const;\n/** Every event Tappify emits, which a mount or a server can subscribe to. */\nexport type HostEventName = (typeof EVENTS)[number];\n\nexport const SERVED_SDK_MAJORS: readonly number[] = [2];\n\n/** The `kind` of every read `tap.data.query` accepts, one per scope it needs. */\nexport type DataQueryKind =\n | 'project'\n | 'series'\n | 'keywords'\n | 'listing'\n | 'reviews'\n | 'crashes'\n | 'revenue';\n\nexport const QUERY_SCOPES: Record<DataQueryKind, ScopeKey> = {\n project: 'projects:read',\n series: 'analytics:read',\n keywords: 'store.metadata:read',\n listing: 'store.metadata:read',\n reviews: 'reviews:read',\n crashes: 'crashes:read',\n revenue: 'revenue:read',\n};\n\n/**\n * The keys a tool's answer carries for each `returns` kind it may declare.\n * `validateToolResponse`, `tappify extension doctor` and the host all read\n * this one table, so a wrong body fails the same way wherever it is checked.\n */\nexport const CARD_KEYS: Record<ToolDeclaration['returns'], string[]> = {\n value: ['label', 'value'],\n series: ['label', 'points'],\n table: ['columns', 'rows'],\n list: ['items'],\n comparison: ['label', 'left', 'right'],\n};\n\nexport const findScope = (key: string): ScopeDefinition | undefined =>\n SCOPES.find(scope => scope.key === key);\n\nexport const findPage = (key: string): PageDefinition | undefined =>\n PAGES.find(page => page.key === key);\n\nexport const findTable = (key: string): TableDefinition | undefined =>\n TABLES.find(table => table.key === key);\n\nexport const slotsForPage = (page: string): readonly SlotId[] =>\n findPage(page)?.slots ?? [];\n"]}
@@ -0,0 +1,33 @@
1
+ import {
2
+ TapError
3
+ } from "./chunk-CSTYBKZC.mjs";
4
+
5
+ // src/client/context.ts
6
+ import { createContext, useContext } from "react";
7
+ var REGISTRY_KEY = "__tappifyExtensionSdkTapContextV2__";
8
+ var registry = globalThis;
9
+ function resolveContext() {
10
+ const existing = registry[REGISTRY_KEY];
11
+ if (existing) return existing;
12
+ const created = createContext(null);
13
+ created.displayName = "TapContext";
14
+ registry[REGISTRY_KEY] = created;
15
+ return created;
16
+ }
17
+ var TapContext = resolveContext();
18
+ function useTap() {
19
+ const tap = useContext(TapContext);
20
+ if (tap === null) {
21
+ throw new TapError(
22
+ "TAP_OUTSIDE_HOST",
23
+ "useTap() ran outside a Tappify mount. Render this component from an entry declared in tappify.extension.json, or wrap it in renderWithTap() from @tappify/extension-sdk/testing."
24
+ );
25
+ }
26
+ return tap;
27
+ }
28
+
29
+ export {
30
+ TapContext,
31
+ useTap
32
+ };
33
+ //# sourceMappingURL=chunk-QHM7XNRK.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/client/context.ts"],"sourcesContent":["import { createContext, useContext, type Context } from 'react';\nimport { TapError } from './errors';\nimport type { Tap } from './types';\n\nconst REGISTRY_KEY = '__tappifyExtensionSdkTapContextV2__';\n\ninterface ContextRegistry {\n [REGISTRY_KEY]?: Context<Tap | null>;\n}\n\nconst registry: typeof globalThis & ContextRegistry = globalThis;\n\nfunction resolveContext(): Context<Tap | null> {\n const existing = registry[REGISTRY_KEY];\n if (existing) return existing;\n\n const created = createContext<Tap | null>(null);\n created.displayName = 'TapContext';\n registry[REGISTRY_KEY] = created;\n return created;\n}\n\nexport const TapContext: Context<Tap | null> = resolveContext();\n\n/**\n * Returns the whole bridge the host mounted this component with.\n *\n * @remarks\n * Throws `TapError` with code `TAP_OUTSIDE_HOST` when no Tappify mount is above\n * the component. The object it returns does not re-render the component when the\n * host changes a member; read through `useTapFilters`, `useTapTheme` and the\n * other slice hooks where that matters.\n *\n * @example\n * ```tsx\n * import { TapButton, useTap } from '@tappify/extension-sdk';\n *\n * function CopyId() {\n * const tap = useTap();\n * return (\n * <TapButton onClick={() => void tap.ui.copy(tap.project.id)}>\n * Copy project id\n * </TapButton>\n * );\n * }\n * ```\n */\nexport function useTap(): Tap {\n const tap = useContext(TapContext);\n if (tap === null) {\n throw new TapError(\n 'TAP_OUTSIDE_HOST',\n 'useTap() ran outside a Tappify mount. Render this component from an entry declared in tappify.extension.json, or wrap it in renderWithTap() from @tappify/extension-sdk/testing.',\n );\n }\n return tap;\n}\n"],"mappings":";;;;;AAAA,SAAS,eAAe,kBAAgC;AAIxD,IAAM,eAAe;AAMrB,IAAM,WAAgD;AAEtD,SAAS,iBAAsC;AAC7C,QAAM,WAAW,SAAS,YAAY;AACtC,MAAI,SAAU,QAAO;AAErB,QAAM,UAAU,cAA0B,IAAI;AAC9C,UAAQ,cAAc;AACtB,WAAS,YAAY,IAAI;AACzB,SAAO;AACT;AAEO,IAAM,aAAkC,eAAe;AAyBvD,SAAS,SAAc;AAC5B,QAAM,MAAM,WAAW,UAAU;AACjC,MAAI,QAAQ,MAAM;AAChB,UAAM,IAAI;AAAA,MACR;AAAA,MACA;AAAA,IACF;AAAA,EACF;AACA,SAAO;AACT;","names":[]}
@@ -0,0 +1,365 @@
1
+ "use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _nullishCoalesce(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return rhsFn(); } } function _optionalChain(ops) { let lastAccessLHS = undefined; let value = ops[0]; let i = 1; while (i < ops.length) { const op = ops[i]; const fn = ops[i + 1]; i += 2; if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { return undefined; } if (op === 'access' || op === 'optionalAccess') { lastAccessLHS = value; value = fn(value); } else if (op === 'call' || op === 'optionalCall') { value = fn((...args) => value.call(lastAccessLHS, ...args)); lastAccessLHS = undefined; } } return value; }
2
+
3
+ var _chunkPYJMJF44cjs = require('./chunk-PYJMJF44.cjs');
4
+
5
+
6
+
7
+ var _chunk2DBSVKAUcjs = require('./chunk-2DBSVKAU.cjs');
8
+
9
+ // src/server/codes.ts
10
+ var SERVER_ERROR_CODES = {
11
+ ROUTE_UNKNOWN: "TAP_ROUTE_UNKNOWN",
12
+ HANDLER_MISSING: "TAP_HANDLER_MISSING",
13
+ INTERNAL_ERROR: "TAP_INTERNAL_ERROR",
14
+ TOKEN_MISSING: "TAP_TOKEN_MISSING",
15
+ TOKEN_INVALID: "TAP_TOKEN_INVALID",
16
+ TOKEN_AUDIENCE: "TAP_TOKEN_AUDIENCE",
17
+ EVENT_ID_MISSING: "TAP_EVENT_ID_MISSING",
18
+ BODY_INVALID: "TAP_BODY_INVALID",
19
+ BODY_TOO_LARGE: "TAP_BODY_TOO_LARGE",
20
+ WEBHOOK_REJECTED: "TAP_WEBHOOK_REJECTED"
21
+ };
22
+
23
+ // src/server/token.ts
24
+
25
+
26
+
27
+
28
+ var _jose = require('jose');
29
+ var DEFAULT_JWKS_URL = "https://api.tappify.ai/api/v1/extensions/.well-known/jwks.json";
30
+ var CLOCK_TOLERANCE_SECONDS = 60;
31
+ var remoteSets = /* @__PURE__ */ new Map();
32
+ function keySet(options) {
33
+ if (options.jwks) return _jose.createLocalJWKSet.call(void 0, options.jwks);
34
+ const url = _nullishCoalesce(options.jwksUrl, () => ( DEFAULT_JWKS_URL));
35
+ const cached = remoteSets.get(url);
36
+ if (cached) return cached;
37
+ const created = _jose.createRemoteJWKSet.call(void 0, new URL(url));
38
+ remoteSets.set(url, created);
39
+ return created;
40
+ }
41
+ function readString(payload, key) {
42
+ const value = payload[key];
43
+ return typeof value === "string" ? value : void 0;
44
+ }
45
+ function toClaims(payload, audience) {
46
+ const aud = payload.aud;
47
+ const matches = aud === audience || Array.isArray(aud) && aud.includes(audience);
48
+ if (!matches) {
49
+ throw new (0, _chunk2DBSVKAUcjs.TapError)(
50
+ "TAP_TOKEN_AUDIENCE",
51
+ `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}".`
52
+ );
53
+ }
54
+ const installId = readString(payload, "installId");
55
+ const extensionId = readString(payload, "extensionId");
56
+ const organizationId = readString(payload, "organizationId");
57
+ if (installId === void 0 || extensionId === void 0 || organizationId === void 0) {
58
+ throw new (0, _chunk2DBSVKAUcjs.TapError)(
59
+ "TAP_TOKEN_INVALID",
60
+ `The install token is missing the installId, extensionId or organizationId claim. Verify against ${DEFAULT_JWKS_URL} and do not reuse tokens across environments.`
61
+ );
62
+ }
63
+ const scopes = payload.scopes;
64
+ const userId = readString(payload, "userId");
65
+ const claims = {
66
+ iss: _nullishCoalesce(readString(payload, "iss"), () => ( "")),
67
+ aud: audience,
68
+ exp: typeof payload.exp === "number" ? payload.exp : 0,
69
+ iat: typeof payload.iat === "number" ? payload.iat : 0,
70
+ installId,
71
+ extensionId,
72
+ projectId: _nullishCoalesce(readString(payload, "projectId"), () => ( null)),
73
+ organizationId,
74
+ scopes: Array.isArray(scopes) ? scopes.filter((scope) => typeof scope === "string") : []
75
+ };
76
+ if (userId !== void 0) claims.userId = userId;
77
+ return claims;
78
+ }
79
+ async function verifyTappifyToken(token, options) {
80
+ const audience = `ext:${options.extensionId}`;
81
+ let payload;
82
+ try {
83
+ const verified = await _jose.jwtVerify.call(void 0, token, keySet(options), {
84
+ algorithms: ["RS256"],
85
+ clockTolerance: CLOCK_TOLERANCE_SECONDS
86
+ });
87
+ payload = verified.payload;
88
+ } catch (e) {
89
+ throw new (0, _chunk2DBSVKAUcjs.TapError)(
90
+ "TAP_TOKEN_INVALID",
91
+ "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."
92
+ );
93
+ }
94
+ return toClaims(payload, audience);
95
+ }
96
+
97
+ // src/server/wire.ts
98
+ var INTERNAL_MESSAGE = "The extension server failed to handle this call. Check your own logs; Tappify records the failure against your extension.";
99
+ function json(data, status = 200) {
100
+ return new Response(JSON.stringify(data), {
101
+ status,
102
+ headers: { "content-type": "application/json" }
103
+ });
104
+ }
105
+ function wireError(code, message, status) {
106
+ return json({ error: { code, message } }, status);
107
+ }
108
+ function failure(code, message, status) {
109
+ return wireError(code, message, status);
110
+ }
111
+ function readUnknown(value, key) {
112
+ if (typeof value !== "object" || value === null) return void 0;
113
+ return value[key];
114
+ }
115
+ function readRecord(value, key) {
116
+ const nested = readUnknown(value, key);
117
+ return typeof nested === "object" && nested !== null ? nested : {};
118
+ }
119
+ function readText(value, key) {
120
+ const nested = readUnknown(value, key);
121
+ return typeof nested === "string" ? nested : null;
122
+ }
123
+
124
+ // src/server/handler.ts
125
+ var MAX_BODY_BYTES = 1048576;
126
+ var WORK_OPERATIONS = [
127
+ "list_containers",
128
+ "create",
129
+ "attach",
130
+ "comment",
131
+ "status",
132
+ "resolve"
133
+ ];
134
+ var TOKEN_FAILURES = [
135
+ SERVER_ERROR_CODES.TOKEN_INVALID,
136
+ SERVER_ERROR_CODES.TOKEN_AUDIENCE
137
+ ];
138
+ function toInstall(payload, claims) {
139
+ const raw = readRecord(payload, "install");
140
+ return {
141
+ id: claims.installId,
142
+ extensionId: claims.extensionId,
143
+ projectId: _nullishCoalesce(claims.projectId, () => ( readText(raw, "projectId"))),
144
+ organizationId: claims.organizationId
145
+ };
146
+ }
147
+ function toPlatform(value) {
148
+ return value === "ios" || value === "android" ? value : "all";
149
+ }
150
+ function toPreset(value) {
151
+ return value === "7d" || value === "30d" || value === "90d" ? value : void 0;
152
+ }
153
+ function toContext(payload) {
154
+ const raw = readRecord(payload, "context");
155
+ const filters = readRecord(raw, "filters");
156
+ const rawRange = readRecord(filters, "range");
157
+ const from = _nullishCoalesce(_nullishCoalesce(readText(raw, "from"), () => ( readText(rawRange, "from"))), () => ( ""));
158
+ const to = _nullishCoalesce(_nullishCoalesce(readText(raw, "to"), () => ( readText(rawRange, "to"))), () => ( ""));
159
+ const range = {
160
+ from: _nullishCoalesce(readText(rawRange, "from"), () => ( from)),
161
+ to: _nullishCoalesce(readText(rawRange, "to"), () => ( to))
162
+ };
163
+ const preset = toPreset(readText(rawRange, "preset"));
164
+ if (preset !== void 0) range.preset = preset;
165
+ return {
166
+ projectId: readText(raw, "projectId"),
167
+ platform: toPlatform(readText(raw, "platform")),
168
+ from,
169
+ to,
170
+ filters: {
171
+ range,
172
+ platform: toPlatform(readText(filters, "platform")),
173
+ country: _nullishCoalesce(readText(filters, "country"), () => ( "all"))
174
+ }
175
+ };
176
+ }
177
+ function toCredentials(payload) {
178
+ const raw = readRecord(payload, "credentials");
179
+ const credentials = {};
180
+ for (const [name, value] of Object.entries(raw)) {
181
+ if (typeof value === "string") credentials[name] = value;
182
+ }
183
+ return credentials;
184
+ }
185
+ function tooLarge() {
186
+ return new (0, _chunk2DBSVKAUcjs.TapServerError)(
187
+ SERVER_ERROR_CODES.BODY_TOO_LARGE,
188
+ "A Tappify request body is at most 1 MB. Return a cursor or an id the host can follow instead of inlining the payload.",
189
+ 413
190
+ );
191
+ }
192
+ async function readBody(request) {
193
+ if (request.method === "GET" || request.method === "HEAD") return {};
194
+ const length = request.headers.get("content-length");
195
+ if (length !== null && Number(length) > MAX_BODY_BYTES) throw tooLarge();
196
+ const bytes = await request.arrayBuffer();
197
+ if (bytes.byteLength > MAX_BODY_BYTES) throw tooLarge();
198
+ if (bytes.byteLength === 0) return {};
199
+ const text = new TextDecoder().decode(bytes);
200
+ try {
201
+ return JSON.parse(text);
202
+ } catch (e2) {
203
+ throw new (0, _chunk2DBSVKAUcjs.TapServerError)(
204
+ SERVER_ERROR_CODES.BODY_INVALID,
205
+ "The request body was not JSON. Do not parse or re-encode the body before handing the Request to createTappifyHandler().",
206
+ 400
207
+ );
208
+ }
209
+ }
210
+ function route(handler, input, event, noContent = false) {
211
+ return { handler, input, event, noContent };
212
+ }
213
+ function handlerFor(map, name) {
214
+ return _optionalChain([map, 'optionalAccess', _ => _[name]]);
215
+ }
216
+ function resolveRoute(options, method, segments, url, payload) {
217
+ const [head, name] = segments;
218
+ const bodyInput = _nullishCoalesce(readUnknown(payload, "input"), () => ( {}));
219
+ if (head === "metrics" && method === "POST") {
220
+ return route(options.metrics, bodyInput);
221
+ }
222
+ if (head === "tools" && method === "POST" && name !== void 0) {
223
+ return route(handlerFor(options.tools, name), bodyInput);
224
+ }
225
+ if (head === "mentions" && method === "GET" && name !== void 0) {
226
+ return route(handlerFor(options.mentions, name), {
227
+ q: _nullishCoalesce(url.searchParams.get("q"), () => ( ""))
228
+ });
229
+ }
230
+ if (head === "actions" && method === "POST" && name !== void 0) {
231
+ return route(handlerFor(options.actions, name), bodyInput);
232
+ }
233
+ if (head === "procedures" && method === "POST" && name !== void 0) {
234
+ return route(handlerFor(options.procedures, name), bodyInput);
235
+ }
236
+ if (head === "context" && method === "GET" && name !== void 0) {
237
+ return route(handlerFor(options.context, name), void 0);
238
+ }
239
+ if (head === "work" && method === "POST" && name !== void 0) {
240
+ const operation = WORK_OPERATIONS.find((candidate) => candidate === name);
241
+ return route(
242
+ operation === void 0 ? void 0 : _optionalChain([options, 'access', _2 => _2.work, 'optionalAccess', _3 => _3[operation]]),
243
+ bodyInput
244
+ );
245
+ }
246
+ if (head === "events" && method === "POST") {
247
+ const envelope = readRecord(payload, "event");
248
+ const eventName = readText(envelope, "name");
249
+ if (eventName === null) {
250
+ throw new (0, _chunk2DBSVKAUcjs.TapServerError)(
251
+ SERVER_ERROR_CODES.BODY_INVALID,
252
+ 'The events body has no event.name. Post the envelope Tappify sends unchanged, with the event name under "event".',
253
+ 400
254
+ );
255
+ }
256
+ const declared = _chunkPYJMJF44cjs.EVENTS.find((candidate) => candidate === eventName);
257
+ return route(
258
+ declared === void 0 ? void 0 : _optionalChain([options, 'access', _4 => _4.events, 'optionalAccess', _5 => _5[declared]]),
259
+ void 0,
260
+ _nullishCoalesce(readUnknown(envelope, "payload"), () => ( {})),
261
+ true
262
+ );
263
+ }
264
+ return null;
265
+ }
266
+ function createTappifyHandler(options) {
267
+ const basePath = _nullishCoalesce(options.basePath, () => ( ""));
268
+ return async function handle(request) {
269
+ const url = new URL(request.url);
270
+ const path = url.pathname.startsWith(basePath) ? url.pathname.slice(basePath.length) : url.pathname;
271
+ const segments = path.split("/").filter((segment) => segment !== "");
272
+ if (segments[0] !== "tappify") {
273
+ return failure(
274
+ SERVER_ERROR_CODES.ROUTE_UNKNOWN,
275
+ `Nothing is mounted at ${path}. Tappify calls paths under /tappify; set basePath if your framework mounts the handler under a prefix.`,
276
+ 404
277
+ );
278
+ }
279
+ const rest = segments.slice(1);
280
+ if (rest[0] === "health" && request.method === "GET") {
281
+ const health = options.health ? await options.health() : { ok: true };
282
+ return json(health);
283
+ }
284
+ const authorization = _nullishCoalesce(request.headers.get("authorization"), () => ( ""));
285
+ const token = authorization.startsWith("Bearer ") ? authorization.slice("Bearer ".length) : "";
286
+ if (token === "") {
287
+ return failure(
288
+ SERVER_ERROR_CODES.TOKEN_MISSING,
289
+ "This route needs an install token. Tappify sends it as Authorization: Bearer <token>; forward the header unchanged from your framework.",
290
+ 401
291
+ );
292
+ }
293
+ const eventId = request.headers.get("x-tappify-event-id");
294
+ if (eventId === null || eventId === "") {
295
+ return failure(
296
+ SERVER_ERROR_CODES.EVENT_ID_MISSING,
297
+ "Every Tappify call carries X-Tappify-Event-Id for idempotency. Forward the header unchanged, and use it as the key when you deduplicate.",
298
+ 400
299
+ );
300
+ }
301
+ try {
302
+ const claims = await verifyTappifyToken(token, {
303
+ extensionId: options.extensionId,
304
+ jwksUrl: options.jwksUrl,
305
+ jwks: options.jwks
306
+ });
307
+ const payload = await readBody(request);
308
+ const matched = resolveRoute(options, request.method, rest, url, payload);
309
+ if (matched === null) {
310
+ return failure(
311
+ SERVER_ERROR_CODES.ROUTE_UNKNOWN,
312
+ `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.`,
313
+ 404
314
+ );
315
+ }
316
+ if (matched.handler === void 0) {
317
+ return failure(
318
+ SERVER_ERROR_CODES.HANDLER_MISSING,
319
+ `No handler is registered for ${request.method} ${path}. Add it to createTappifyHandler(), or remove the contribution from tappify.extension.json.`,
320
+ 404
321
+ );
322
+ }
323
+ const documents = readRecord(payload, "documents");
324
+ const typed = {
325
+ install: toInstall(payload, claims),
326
+ context: toContext(payload),
327
+ input: matched.input,
328
+ credentials: toCredentials(payload),
329
+ documents,
330
+ event: matched.event,
331
+ eventId,
332
+ claims,
333
+ request
334
+ };
335
+ const result = await matched.handler(typed);
336
+ return matched.noContent ? new Response(null, { status: 204 }) : json(result);
337
+ } catch (error) {
338
+ if (_chunk2DBSVKAUcjs.TapServerError.is(error)) {
339
+ return wireError(error.code, error.message, _nullishCoalesce(error.status, () => ( 400)));
340
+ }
341
+ if (_chunk2DBSVKAUcjs.TapError.is(error)) {
342
+ const tokenFailure = TOKEN_FAILURES.find(
343
+ (candidate) => candidate === error.code
344
+ );
345
+ if (tokenFailure !== void 0) {
346
+ return failure(tokenFailure, error.message, 401);
347
+ }
348
+ return wireError(error.code, error.message, 400);
349
+ }
350
+ return failure(SERVER_ERROR_CODES.INTERNAL_ERROR, INTERNAL_MESSAGE, 500);
351
+ }
352
+ };
353
+ }
354
+
355
+
356
+
357
+
358
+
359
+
360
+
361
+
362
+
363
+
364
+ exports.SERVER_ERROR_CODES = SERVER_ERROR_CODES; exports.INTERNAL_MESSAGE = INTERNAL_MESSAGE; exports.wireError = wireError; exports.readRecord = readRecord; exports.readText = readText; exports.DEFAULT_JWKS_URL = DEFAULT_JWKS_URL; exports.verifyTappifyToken = verifyTappifyToken; exports.createTappifyHandler = createTappifyHandler;
365
+ //# sourceMappingURL=chunk-RMUWRESK.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["/Users/vulakh/Desktop/Work/Tappify/extension-sdk/dist/chunk-RMUWRESK.cjs","../src/server/codes.ts","../src/server/token.ts","../src/server/wire.ts","../src/server/handler.ts"],"names":[],"mappings":"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","file":"/Users/vulakh/Desktop/Work/Tappify/extension-sdk/dist/chunk-RMUWRESK.cjs","sourcesContent":[null,"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 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[] = [\n 'list_containers',\n 'create',\n 'attach',\n 'comment',\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 return route(\n operation === undefined ? undefined : options.work?.[operation],\n bodyInput,\n );\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"]}