@tappify/extension-sdk 2.1.0-beta.2 → 2.1.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 (137) hide show
  1. package/README.md +18 -54
  2. package/dist/{chunk-6HETNZJY.cjs → chunk-2DBSVKAU.cjs} +1 -1
  3. package/dist/{chunk-6HETNZJY.cjs.map → chunk-2DBSVKAU.cjs.map} +1 -1
  4. package/dist/{chunk-O357OJXL.cjs → chunk-3YWVYA42.cjs} +18 -14
  5. package/dist/chunk-3YWVYA42.cjs.map +1 -0
  6. package/dist/{chunk-GAUZYBXE.cjs → chunk-5AAPD5WV.cjs} +11 -11
  7. package/dist/chunk-5AAPD5WV.cjs.map +1 -0
  8. package/dist/{chunk-SGEYDV3K.mjs → chunk-BDCX6LRQ.mjs} +3 -3
  9. package/dist/chunk-BDCX6LRQ.mjs.map +1 -0
  10. package/dist/{chunk-KZ5NGFUP.mjs → chunk-C3YNVHBP.mjs} +7 -20
  11. package/dist/chunk-C3YNVHBP.mjs.map +1 -0
  12. package/dist/{chunk-QXD7WVKK.mjs → chunk-CSTYBKZC.mjs} +1 -1
  13. package/dist/chunk-CSTYBKZC.mjs.map +1 -0
  14. package/dist/{chunk-SWG7CNTZ.mjs → chunk-CTSZ5D7S.mjs} +5 -9
  15. package/dist/chunk-CTSZ5D7S.mjs.map +1 -0
  16. package/dist/chunk-EW54H7CR.cjs +722 -0
  17. package/dist/chunk-EW54H7CR.cjs.map +1 -0
  18. package/dist/chunk-EX3NFXDK.mjs +722 -0
  19. package/dist/chunk-EX3NFXDK.mjs.map +1 -0
  20. package/dist/{chunk-A74HCO6T.cjs → chunk-G7XZSN2Q.cjs} +18 -31
  21. package/dist/chunk-G7XZSN2Q.cjs.map +1 -0
  22. package/dist/{chunk-RBIQT75L.mjs → chunk-GG3IX52W.mjs} +2 -2
  23. package/dist/{chunk-SLGOHIBL.mjs → chunk-IGK2AP6J.mjs} +17 -13
  24. package/dist/chunk-IGK2AP6J.mjs.map +1 -0
  25. package/dist/{chunk-VV7ELNRW.cjs → chunk-JMUK3K47.cjs} +3 -3
  26. package/dist/{chunk-VV7ELNRW.cjs.map → chunk-JMUK3K47.cjs.map} +1 -1
  27. package/dist/{chunk-SGCYEEXD.mjs → chunk-MNDVYPEP.mjs} +2 -9
  28. package/dist/chunk-MNDVYPEP.mjs.map +1 -0
  29. package/dist/{chunk-BPCGXXMD.cjs → chunk-NFUPPCX4.cjs} +18 -22
  30. package/dist/chunk-NFUPPCX4.cjs.map +1 -0
  31. package/dist/{chunk-PRCZMOLD.cjs → chunk-OPPYEA7F.cjs} +3 -3
  32. package/dist/{chunk-PRCZMOLD.cjs.map → chunk-OPPYEA7F.cjs.map} +1 -1
  33. package/dist/{chunk-7YKH4LZN.cjs → chunk-PYJMJF44.cjs} +2 -9
  34. package/dist/chunk-PYJMJF44.cjs.map +1 -0
  35. package/dist/{chunk-QA5AL3VZ.mjs → chunk-QHM7XNRK.mjs} +2 -2
  36. package/dist/{chunk-QA5AL3VZ.mjs.map → chunk-QHM7XNRK.mjs.map} +1 -1
  37. package/dist/codegen.cjs.map +1 -1
  38. package/dist/codegen.d.cts +7 -43
  39. package/dist/codegen.d.ts +7 -43
  40. package/dist/codegen.mjs.map +1 -1
  41. package/dist/{constants-Bh759FUI.d.cts → constants-DJ_KgZpN.d.cts} +8 -44
  42. package/dist/{constants-Bh759FUI.d.ts → constants-DJ_KgZpN.d.ts} +8 -44
  43. package/dist/{context-mr8R5CNt.d.ts → context-Dt6xh_5t.d.cts} +4 -5
  44. package/dist/{context-DI0EJCRD.d.cts → context-Ml7k_V14.d.ts} +4 -5
  45. package/dist/{errors-Cqh0wrqt.d.ts → errors-COsBh7kd.d.cts} +2 -3
  46. package/dist/{errors-Cqh0wrqt.d.cts → errors-COsBh7kd.d.ts} +2 -3
  47. package/dist/handler-CZIlZ5vu.d.ts +227 -0
  48. package/dist/handler-kU9Jf36K.d.cts +227 -0
  49. package/dist/host.cjs +4 -4
  50. package/dist/host.d.cts +4 -4
  51. package/dist/host.d.ts +4 -4
  52. package/dist/host.mjs +3 -3
  53. package/dist/index.cjs +34 -5
  54. package/dist/index.cjs.map +1 -1
  55. package/dist/index.d.cts +627 -69
  56. package/dist/index.d.ts +627 -69
  57. package/dist/index.mjs +33 -4
  58. package/dist/manifest.cjs +24 -136
  59. package/dist/manifest.cjs.map +1 -1
  60. package/dist/manifest.d.cts +12 -97
  61. package/dist/manifest.d.ts +12 -97
  62. package/dist/manifest.mjs +10 -122
  63. package/dist/manifest.mjs.map +1 -1
  64. package/dist/server.cjs +66 -14
  65. package/dist/server.cjs.map +1 -1
  66. package/dist/server.d.cts +6 -6
  67. package/dist/server.d.ts +6 -6
  68. package/dist/server.mjs +61 -9
  69. package/dist/server.mjs.map +1 -1
  70. package/dist/styles.css +367 -0
  71. package/dist/testing/index.cjs +13 -72
  72. package/dist/testing/index.cjs.map +1 -1
  73. package/dist/testing/index.d.cts +10 -29
  74. package/dist/testing/index.d.ts +10 -29
  75. package/dist/testing/index.mjs +13 -72
  76. package/dist/testing/index.mjs.map +1 -1
  77. package/dist/testing/jest.cjs +5 -5
  78. package/dist/testing/jest.mjs +2 -2
  79. package/dist/testing/vitest.cjs +5 -5
  80. package/dist/testing/vitest.cjs.map +1 -1
  81. package/dist/testing/vitest.d.cts +0 -10
  82. package/dist/testing/vitest.d.ts +0 -10
  83. package/dist/testing/vitest.mjs +2 -2
  84. package/dist/testing/vitest.mjs.map +1 -1
  85. package/dist/{types-uGHrRaOj.d.ts → types-DDAGLpov.d.ts} +3 -4
  86. package/dist/{types-BdDaLnqs.d.cts → types-DrpQSv_2.d.cts} +3 -4
  87. package/dist/vite-preview.cjs +125 -294
  88. package/dist/vite-preview.cjs.map +1 -1
  89. package/dist/vite-preview.d.cts +24 -6
  90. package/dist/vite-preview.d.ts +24 -6
  91. package/dist/vite-preview.mjs +118 -287
  92. package/dist/vite-preview.mjs.map +1 -1
  93. package/dist/vite.cjs +24 -137
  94. package/dist/vite.cjs.map +1 -1
  95. package/dist/vite.d.cts +7 -43
  96. package/dist/vite.d.ts +7 -43
  97. package/dist/vite.mjs +18 -131
  98. package/dist/vite.mjs.map +1 -1
  99. package/package.json +2 -12
  100. package/schema/extension-v2.json +121 -440
  101. package/dist/chunk-2CY5HVY7.mjs +0 -59
  102. package/dist/chunk-2CY5HVY7.mjs.map +0 -1
  103. package/dist/chunk-7YKH4LZN.cjs.map +0 -1
  104. package/dist/chunk-A74HCO6T.cjs.map +0 -1
  105. package/dist/chunk-BPCGXXMD.cjs.map +0 -1
  106. package/dist/chunk-EB5XWQAU.cjs +0 -132
  107. package/dist/chunk-EB5XWQAU.cjs.map +0 -1
  108. package/dist/chunk-GAUZYBXE.cjs.map +0 -1
  109. package/dist/chunk-KZ5NGFUP.mjs.map +0 -1
  110. package/dist/chunk-O357OJXL.cjs.map +0 -1
  111. package/dist/chunk-ONJMPO3P.cjs +0 -59
  112. package/dist/chunk-ONJMPO3P.cjs.map +0 -1
  113. package/dist/chunk-QJMZRV7A.mjs +0 -132
  114. package/dist/chunk-QJMZRV7A.mjs.map +0 -1
  115. package/dist/chunk-QXD7WVKK.mjs.map +0 -1
  116. package/dist/chunk-SGCYEEXD.mjs.map +0 -1
  117. package/dist/chunk-SGEYDV3K.mjs.map +0 -1
  118. package/dist/chunk-SLGOHIBL.mjs.map +0 -1
  119. package/dist/chunk-SWG7CNTZ.mjs.map +0 -1
  120. package/dist/client-BO3kavn0.d.ts +0 -25
  121. package/dist/client-DE-vMgdO.d.cts +0 -25
  122. package/dist/ui/styles.css +0 -740
  123. package/dist/ui.cjs +0 -1018
  124. package/dist/ui.cjs.map +0 -1
  125. package/dist/ui.d.cts +0 -231
  126. package/dist/ui.d.ts +0 -231
  127. package/dist/ui.mjs +0 -1018
  128. package/dist/ui.mjs.map +0 -1
  129. package/dist/vite-preview-bootstrap.cjs +0 -66
  130. package/dist/vite-preview-bootstrap.cjs.map +0 -1
  131. package/dist/vite-preview-bootstrap.d.cts +0 -15
  132. package/dist/vite-preview-bootstrap.d.ts +0 -15
  133. package/dist/vite-preview-bootstrap.mjs +0 -66
  134. package/dist/vite-preview-bootstrap.mjs.map +0 -1
  135. package/dist/work-events-BlOVpK0k.d.ts +0 -547
  136. package/dist/work-events-D97EoqvS.d.cts +0 -547
  137. /package/dist/{chunk-RBIQT75L.mjs.map → chunk-GG3IX52W.mjs.map} +0 -0
package/README.md CHANGED
@@ -26,8 +26,7 @@ The SDK requires Node.js 20 or newer and React 19.
26
26
 
27
27
  | Import | Purpose |
28
28
  | --------------------------------------- | ---------------------------------------------------------------------- |
29
- | `@tappify/extension-sdk` | React hooks, host types, and query helpers |
30
- | `@tappify/extension-sdk/ui` | One UI components and layout primitives |
29
+ | `@tappify/extension-sdk` | React hooks, host types, query helpers, and UI components |
31
30
  | `@tappify/extension-sdk/manifest` | Manifest schema, validation, registry constants, and manifest types |
32
31
  | `@tappify/extension-sdk/server` | Request handler, token verification, events, and Node/Express adapters |
33
32
  | `@tappify/extension-sdk/vite` | Vite and Module Federation configuration |
@@ -35,7 +34,7 @@ The SDK requires Node.js 20 or newer and React 19.
35
34
  | `@tappify/extension-sdk/testing/vitest` | Vitest matcher setup |
36
35
  | `@tappify/extension-sdk/testing/jest` | Jest matcher setup |
37
36
  | `@tappify/extension-sdk/codegen` | Manifest-driven TypeScript generation |
38
- | `@tappify/extension-sdk/ui/styles.css` | One UI component styles |
37
+ | `@tappify/extension-sdk/styles.css` | Base styles for SDK UI components |
39
38
 
40
39
  ## Build a contribution
41
40
 
@@ -45,19 +44,14 @@ available only when the manifest declares the corresponding scope.
45
44
 
46
45
  ```tsx
47
46
  import {
47
+ TapCard,
48
+ TapSkeleton,
49
+ TapStat,
48
50
  useTapFilters,
49
51
  useTapQuery,
50
52
  type TapWidgetProps,
51
53
  } from '@tappify/extension-sdk';
52
- import {
53
- Card,
54
- CardContent,
55
- CardHeader,
56
- CardTitle,
57
- Metric,
58
- Skeleton,
59
- } from '@tappify/extension-sdk/ui';
60
- import '@tappify/extension-sdk/ui/styles.css';
54
+ import '@tappify/extension-sdk/styles.css';
61
55
 
62
56
  export default function DownloadsWidget(_props: TapWidgetProps) {
63
57
  const filters = useTapFilters();
@@ -68,52 +62,22 @@ export default function DownloadsWidget(_props: TapWidgetProps) {
68
62
  });
69
63
 
70
64
  return (
71
- <Card>
72
- <CardHeader>
73
- <CardTitle>Downloads</CardTitle>
74
- </CardHeader>
75
- <CardContent>
76
- {downloads.isLoading && <Skeleton lines={1} />}
77
- {downloads.data && (
78
- <Metric
79
- label="Total"
80
- value={downloads.data.points.reduce(
81
- (total, point) => total + point.value,
82
- 0,
83
- )}
84
- />
85
- )}
86
- </CardContent>
87
- </Card>
65
+ <TapCard title="Downloads">
66
+ {downloads.isLoading && <TapSkeleton lines={1} />}
67
+ {downloads.data && (
68
+ <TapStat
69
+ label="Total"
70
+ value={downloads.data.points.reduce(
71
+ (total, point) => total + point.value,
72
+ 0,
73
+ )}
74
+ />
75
+ )}
76
+ </TapCard>
88
77
  );
89
78
  }
90
79
  ```
91
80
 
92
- ## Migrating UI imports
93
-
94
- Version 2 replaces the legacy UI surface with One UI. Import frontend components
95
- from `@tappify/extension-sdk/ui` and load
96
- `@tappify/extension-sdk/ui/styles.css`. The package root remains the entry for
97
- hooks, host types, and query helpers.
98
-
99
- ```tsx
100
- // Before
101
- import { TapButton, TapCard } from '@tappify/extension-sdk';
102
- import '@tappify/extension-sdk/styles.css';
103
-
104
- // After
105
- import { Button, Card } from '@tappify/extension-sdk/ui';
106
- import '@tappify/extension-sdk/ui/styles.css';
107
- ```
108
-
109
- Removed names map as follows: `TapButton` → `Button`, `TapCard` → the compound
110
- `Card` parts, `TapPageHeader` → `PageHeader`, `TapSkeleton` → `Skeleton`,
111
- `TapEmptyState` → `EmptyState`, `TapErrorState` → `ErrorState`, `TapStat` →
112
- `Metric`, `TapInput` → `Field` with `Input` or `Textarea`, `TapSelect` → `Field`
113
- with `Select`, `TapTable` → the compound `Table` parts, `TapDialog` → the
114
- compound `Dialog` parts, `TapForm` → `SchemaForm`, and `formFields` →
115
- `schemaFields`. No compatibility aliases are retained.
116
-
117
81
  Add the SDK plugin to the extension's Vite config:
118
82
 
119
83
  ```ts
@@ -62,4 +62,4 @@ var TapServerError = class extends Error {
62
62
 
63
63
 
64
64
  exports.TAP_ERROR_CODES = TAP_ERROR_CODES; exports.TapError = TapError; exports.TapServerError = TapServerError;
65
- //# sourceMappingURL=chunk-6HETNZJY.cjs.map
65
+ //# sourceMappingURL=chunk-2DBSVKAU.cjs.map
@@ -1 +1 @@
1
- {"version":3,"sources":["/home/runner/work/extension-sdk/extension-sdk/dist/chunk-6HETNZJY.cjs","../src/client/errors.ts"],"names":[],"mappings":"AAAA;AC4BO,IAAM,gBAAA,EAA2C;AAAA,EACtD,kBAAA;AAAA,EACA,mBAAA;AAAA,EACA,wBAAA;AAAA,EACA,uBAAA;AAAA,EACA,eAAA;AAAA,EACA,qBAAA;AAAA,EACA,sBAAA;AAAA,EACA,mBAAA;AAAA,EACA,uBAAA;AAAA,EACA,uBAAA;AAAA,EACA,mBAAA;AAAA,EACA,mBAAA;AAAA,EACA,oBAAA;AAAA,EACA,sBAAA;AAAA,EACA,kBAAA;AAAA,EACA,oBAAA;AAAA,EACA,mBAAA;AAAA,EACA,qBAAA;AAAA,EACA,oBAAA;AAAA,EACA,sBAAA;AAAA,EACA;AACF,CAAA;AAEA,IAAM,SAAA,EAAW,IAAI,GAAA,CAAY,eAAe,CAAA;AA+BzC,IAAM,SAAA,EAAN,MAAA,QAAuB,MAAM;AAAA,EAGlC,WAAA,CAAY,IAAA,EAAoB,OAAA,EAAiB;AAC/C,IAAA,KAAA,CAAM,OAAO,CAAA;AACb,IAAA,IAAA,CAAK,KAAA,EAAO,UAAA;AACZ,IAAA,IAAA,CAAK,KAAA,EAAO,IAAA;AAAA,EACd;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,OAAO,EAAA,CAAG,KAAA,EAAmC;AAC3C,IAAA,OACE,MAAA,WAAiB,MAAA,GACjB,KAAA,CAAM,KAAA,IAAS,WAAA,GACf,OAAA,GAAU,MAAA,GACV,OAAO,KAAA,CAAM,KAAA,IAAS,SAAA,GACtB,QAAA,CAAS,GAAA,CAAI,KAAA,CAAM,IAAI,CAAA;AAAA,EAE3B;AACF,CAAA;AA2BO,IAAM,eAAA,EAAN,MAAA,QAA6B,MAAM;AAAA,EAIxC,WAAA,CAAY,IAAA,EAAc,OAAA,EAAiB,MAAA,EAAiB;AAC1D,IAAA,KAAA,CAAM,OAAO,CAAA;AACb,IAAA,IAAA,CAAK,KAAA,EAAO,gBAAA;AACZ,IAAA,IAAA,CAAK,KAAA,EAAO,IAAA;AACZ,IAAA,IAAA,CAAK,OAAA,EAAS,MAAA;AAAA,EAChB;AAAA;AAAA,EAGA,MAAA,CAAA,EAAuD;AACrD,IAAA,OAAO,EAAE,KAAA,EAAO,EAAE,IAAA,EAAM,IAAA,CAAK,IAAA,EAAM,OAAA,EAAS,IAAA,CAAK,QAAQ,EAAE,CAAA;AAAA,EAC7D;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,OAAO,EAAA,CAAG,KAAA,EAAyC;AACjD,IAAA,OACE,MAAA,WAAiB,MAAA,GACjB,KAAA,CAAM,KAAA,IAAS,iBAAA,GACf,OAAA,GAAU,MAAA,GACV,OAAO,KAAA,CAAM,KAAA,IAAS,QAAA;AAAA,EAE1B;AACF,CAAA;ADtGA;AACA;AACE;AACA;AACA;AACF,gHAAC","file":"/home/runner/work/extension-sdk/extension-sdk/dist/chunk-6HETNZJY.cjs","sourcesContent":[null,"/** Every code a `TapError` the SDK raises carries. */\nexport type TapErrorCode =\n | 'TAP_OUTSIDE_HOST'\n | 'TAP_SCOPE_MISSING'\n | 'TAP_UNKNOWN_COLLECTION'\n | 'TAP_UNKNOWN_PROCEDURE'\n | 'TAP_NO_PORTAL'\n | 'TAP_NO_QUERY_CLIENT'\n | 'TAP_MANIFEST_INVALID'\n | 'TAP_ENTRY_MISSING'\n | 'TAP_STYLES_UNRESOLVED'\n | 'TAP_SCHEMA_UNRESOLVED'\n | 'TAP_TOKEN_MISSING'\n | 'TAP_TOKEN_INVALID'\n | 'TAP_TOKEN_AUDIENCE'\n | 'TAP_EVENT_ID_MISSING'\n | 'TAP_BODY_INVALID'\n | 'TAP_BODY_TOO_LARGE'\n | 'TAP_ROUTE_UNKNOWN'\n | 'TAP_HANDLER_MISSING'\n | 'TAP_INTERNAL_ERROR'\n | 'TAP_WEBHOOK_REJECTED'\n | 'TAP_TEST_TARGET_INVALID';\n\n/**\n * The same codes as `TapErrorCode`, as an array to iterate or check membership\n * against at runtime.\n */\nexport const TAP_ERROR_CODES: readonly TapErrorCode[] = [\n 'TAP_OUTSIDE_HOST',\n 'TAP_SCOPE_MISSING',\n 'TAP_UNKNOWN_COLLECTION',\n 'TAP_UNKNOWN_PROCEDURE',\n 'TAP_NO_PORTAL',\n 'TAP_NO_QUERY_CLIENT',\n 'TAP_MANIFEST_INVALID',\n 'TAP_ENTRY_MISSING',\n 'TAP_STYLES_UNRESOLVED',\n 'TAP_SCHEMA_UNRESOLVED',\n 'TAP_TOKEN_MISSING',\n 'TAP_TOKEN_INVALID',\n 'TAP_TOKEN_AUDIENCE',\n 'TAP_EVENT_ID_MISSING',\n 'TAP_BODY_INVALID',\n 'TAP_BODY_TOO_LARGE',\n 'TAP_ROUTE_UNKNOWN',\n 'TAP_HANDLER_MISSING',\n 'TAP_INTERNAL_ERROR',\n 'TAP_WEBHOOK_REJECTED',\n 'TAP_TEST_TARGET_INVALID',\n];\n\nconst CODE_SET = new Set<string>(TAP_ERROR_CODES);\n\n/**\n * The error the SDK and the host raise when a call cannot be served at all.\n *\n * @remarks\n * The SDK throws it for a mount outside the host (`TAP_OUTSIDE_HOST`), a\n * collection or procedure the manifest does not declare\n * (`TAP_UNKNOWN_COLLECTION`, `TAP_UNKNOWN_PROCEDURE`), a missing portal or query\n * client, a token the server rejected, and a misused testing helper. The host\n * turns a refused read or write into `TAP_SCOPE_MISSING`. Branch on `code`, which\n * is stable; the message is prose and changes. An error your own server raised is\n * a `TapServerError` instead.\n *\n * @example\n * ```tsx\n * import { TapError, useTapQuery } from '@tappify/extension-sdk';\n * import { ErrorState } from '@tappify/extension-sdk/ui';\n *\n * function Revenue() {\n * const revenue = useTapQuery({\n * kind: 'revenue',\n * range: { from: '2026-09-01', to: '2026-09-08' },\n * });\n * if (TapError.is(revenue.error) && revenue.error.code === 'TAP_SCOPE_MISSING') {\n * return <ErrorState title=\"Revenue is not shared with this extension\" />;\n * }\n * return <span>{revenue.data?.mrr ?? 0}</span>;\n * }\n * ```\n */\nexport class TapError extends Error {\n readonly code: TapErrorCode;\n\n constructor(code: TapErrorCode, message: string) {\n super(message);\n this.name = 'TapError';\n this.code = code;\n }\n\n /**\n * Narrows an unknown value to a `TapError`, by name and by a code it\n * recognises, so it holds across module instances.\n */\n static is(value: unknown): value is TapError {\n return (\n value instanceof Error &&\n value.name === 'TapError' &&\n 'code' in value &&\n typeof value.code === 'string' &&\n CODE_SET.has(value.code)\n );\n }\n}\n\n/**\n * The error a vendor server throws to answer a Tappify call with a code and a\n * message the owner sees.\n *\n * @remarks\n * `createTappifyHandler` catches it and answers `{ error: { code, message } }`\n * with `status`, or 400 when no status was given; the host relays both to the\n * calling mount, where they arrive as the `error` of `useTapServer`. Pick a code\n * of your own and keep it stable — the SDK's own codes are `SERVER_ERROR_CODES` —\n * and write the message for the owner, who reads it on screen.\n *\n * @example\n * ```ts\n * import { TapServerError } from '@tappify/extension-sdk/server';\n *\n * function assertRange(days: number) {\n * if (days > 0) return;\n * throw new TapServerError(\n * 'RANGE_TOO_SHORT',\n * 'getSummary needs a range of at least one day. Widen the date picker, then retry.',\n * 400,\n * );\n * }\n * ```\n */\nexport class TapServerError extends Error {\n readonly code: string;\n readonly status: number | undefined;\n\n constructor(code: string, message: string, status?: number) {\n super(message);\n this.name = 'TapServerError';\n this.code = code;\n this.status = status;\n }\n\n /** The wire body the handler answers with, without the status. */\n toJSON(): { error: { code: string; message: string } } {\n return { error: { code: this.code, message: this.message } };\n }\n\n /**\n * Narrows an unknown value to a `TapServerError` by name and by carrying a\n * string `code`, so it holds across module instances.\n */\n static is(value: unknown): value is TapServerError {\n return (\n value instanceof Error &&\n value.name === 'TapServerError' &&\n 'code' in value &&\n typeof value.code === 'string'\n );\n }\n}\n"]}
1
+ {"version":3,"sources":["/home/runner/work/extension-sdk/extension-sdk/dist/chunk-2DBSVKAU.cjs","../src/client/errors.ts"],"names":[],"mappings":"AAAA;AC4BO,IAAM,gBAAA,EAA2C;AAAA,EACtD,kBAAA;AAAA,EACA,mBAAA;AAAA,EACA,wBAAA;AAAA,EACA,uBAAA;AAAA,EACA,eAAA;AAAA,EACA,qBAAA;AAAA,EACA,sBAAA;AAAA,EACA,mBAAA;AAAA,EACA,uBAAA;AAAA,EACA,uBAAA;AAAA,EACA,mBAAA;AAAA,EACA,mBAAA;AAAA,EACA,oBAAA;AAAA,EACA,sBAAA;AAAA,EACA,kBAAA;AAAA,EACA,oBAAA;AAAA,EACA,mBAAA;AAAA,EACA,qBAAA;AAAA,EACA,oBAAA;AAAA,EACA,sBAAA;AAAA,EACA;AACF,CAAA;AAEA,IAAM,SAAA,EAAW,IAAI,GAAA,CAAY,eAAe,CAAA;AA8BzC,IAAM,SAAA,EAAN,MAAA,QAAuB,MAAM;AAAA,EAGlC,WAAA,CAAY,IAAA,EAAoB,OAAA,EAAiB;AAC/C,IAAA,KAAA,CAAM,OAAO,CAAA;AACb,IAAA,IAAA,CAAK,KAAA,EAAO,UAAA;AACZ,IAAA,IAAA,CAAK,KAAA,EAAO,IAAA;AAAA,EACd;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,OAAO,EAAA,CAAG,KAAA,EAAmC;AAC3C,IAAA,OACE,MAAA,WAAiB,MAAA,GACjB,KAAA,CAAM,KAAA,IAAS,WAAA,GACf,OAAA,GAAU,MAAA,GACV,OAAO,KAAA,CAAM,KAAA,IAAS,SAAA,GACtB,QAAA,CAAS,GAAA,CAAI,KAAA,CAAM,IAAI,CAAA;AAAA,EAE3B;AACF,CAAA;AA2BO,IAAM,eAAA,EAAN,MAAA,QAA6B,MAAM;AAAA,EAIxC,WAAA,CAAY,IAAA,EAAc,OAAA,EAAiB,MAAA,EAAiB;AAC1D,IAAA,KAAA,CAAM,OAAO,CAAA;AACb,IAAA,IAAA,CAAK,KAAA,EAAO,gBAAA;AACZ,IAAA,IAAA,CAAK,KAAA,EAAO,IAAA;AACZ,IAAA,IAAA,CAAK,OAAA,EAAS,MAAA;AAAA,EAChB;AAAA;AAAA,EAGA,MAAA,CAAA,EAAuD;AACrD,IAAA,OAAO,EAAE,KAAA,EAAO,EAAE,IAAA,EAAM,IAAA,CAAK,IAAA,EAAM,OAAA,EAAS,IAAA,CAAK,QAAQ,EAAE,CAAA;AAAA,EAC7D;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,OAAO,EAAA,CAAG,KAAA,EAAyC;AACjD,IAAA,OACE,MAAA,WAAiB,MAAA,GACjB,KAAA,CAAM,KAAA,IAAS,iBAAA,GACf,OAAA,GAAU,MAAA,GACV,OAAO,KAAA,CAAM,KAAA,IAAS,QAAA;AAAA,EAE1B;AACF,CAAA;ADrGA;AACA;AACE;AACA;AACA;AACF,gHAAC","file":"/home/runner/work/extension-sdk/extension-sdk/dist/chunk-2DBSVKAU.cjs","sourcesContent":[null,"/** Every code a `TapError` the SDK raises carries. */\nexport type TapErrorCode =\n | 'TAP_OUTSIDE_HOST'\n | 'TAP_SCOPE_MISSING'\n | 'TAP_UNKNOWN_COLLECTION'\n | 'TAP_UNKNOWN_PROCEDURE'\n | 'TAP_NO_PORTAL'\n | 'TAP_NO_QUERY_CLIENT'\n | 'TAP_MANIFEST_INVALID'\n | 'TAP_ENTRY_MISSING'\n | 'TAP_STYLES_UNRESOLVED'\n | 'TAP_SCHEMA_UNRESOLVED'\n | 'TAP_TOKEN_MISSING'\n | 'TAP_TOKEN_INVALID'\n | 'TAP_TOKEN_AUDIENCE'\n | 'TAP_EVENT_ID_MISSING'\n | 'TAP_BODY_INVALID'\n | 'TAP_BODY_TOO_LARGE'\n | 'TAP_ROUTE_UNKNOWN'\n | 'TAP_HANDLER_MISSING'\n | 'TAP_INTERNAL_ERROR'\n | 'TAP_WEBHOOK_REJECTED'\n | 'TAP_TEST_TARGET_INVALID';\n\n/**\n * The same codes as `TapErrorCode`, as an array to iterate or check membership\n * against at runtime.\n */\nexport const TAP_ERROR_CODES: readonly TapErrorCode[] = [\n 'TAP_OUTSIDE_HOST',\n 'TAP_SCOPE_MISSING',\n 'TAP_UNKNOWN_COLLECTION',\n 'TAP_UNKNOWN_PROCEDURE',\n 'TAP_NO_PORTAL',\n 'TAP_NO_QUERY_CLIENT',\n 'TAP_MANIFEST_INVALID',\n 'TAP_ENTRY_MISSING',\n 'TAP_STYLES_UNRESOLVED',\n 'TAP_SCHEMA_UNRESOLVED',\n 'TAP_TOKEN_MISSING',\n 'TAP_TOKEN_INVALID',\n 'TAP_TOKEN_AUDIENCE',\n 'TAP_EVENT_ID_MISSING',\n 'TAP_BODY_INVALID',\n 'TAP_BODY_TOO_LARGE',\n 'TAP_ROUTE_UNKNOWN',\n 'TAP_HANDLER_MISSING',\n 'TAP_INTERNAL_ERROR',\n 'TAP_WEBHOOK_REJECTED',\n 'TAP_TEST_TARGET_INVALID',\n];\n\nconst CODE_SET = new Set<string>(TAP_ERROR_CODES);\n\n/**\n * The error the SDK and the host raise when a call cannot be served at all.\n *\n * @remarks\n * The SDK throws it for a mount outside the host (`TAP_OUTSIDE_HOST`), a\n * collection or procedure the manifest does not declare\n * (`TAP_UNKNOWN_COLLECTION`, `TAP_UNKNOWN_PROCEDURE`), a missing portal or query\n * client, a token the server rejected, and a misused testing helper. The host\n * turns a refused read or write into `TAP_SCOPE_MISSING`. Branch on `code`, which\n * is stable; the message is prose and changes. An error your own server raised is\n * a `TapServerError` instead.\n *\n * @example\n * ```tsx\n * import { TapError, TapErrorState, useTapQuery } from '@tappify/extension-sdk';\n *\n * function Revenue() {\n * const revenue = useTapQuery({\n * kind: 'revenue',\n * range: { from: '2026-09-01', to: '2026-09-08' },\n * });\n * if (TapError.is(revenue.error) && revenue.error.code === 'TAP_SCOPE_MISSING') {\n * return <TapErrorState title=\"Revenue is not shared with this extension\" />;\n * }\n * return <span>{revenue.data?.mrr ?? 0}</span>;\n * }\n * ```\n */\nexport class TapError extends Error {\n readonly code: TapErrorCode;\n\n constructor(code: TapErrorCode, message: string) {\n super(message);\n this.name = 'TapError';\n this.code = code;\n }\n\n /**\n * Narrows an unknown value to a `TapError`, by name and by a code it\n * recognises, so it holds across module instances.\n */\n static is(value: unknown): value is TapError {\n return (\n value instanceof Error &&\n value.name === 'TapError' &&\n 'code' in value &&\n typeof value.code === 'string' &&\n CODE_SET.has(value.code)\n );\n }\n}\n\n/**\n * The error a vendor server throws to answer a Tappify call with a code and a\n * message the owner sees.\n *\n * @remarks\n * `createTappifyHandler` catches it and answers `{ error: { code, message } }`\n * with `status`, or 400 when no status was given; the host relays both to the\n * calling mount, where they arrive as the `error` of `useTapServer`. Pick a code\n * of your own and keep it stable — the SDK's own codes are `SERVER_ERROR_CODES` —\n * and write the message for the owner, who reads it on screen.\n *\n * @example\n * ```ts\n * import { TapServerError } from '@tappify/extension-sdk/server';\n *\n * function assertRange(days: number) {\n * if (days > 0) return;\n * throw new TapServerError(\n * 'RANGE_TOO_SHORT',\n * 'getSummary needs a range of at least one day. Widen the date picker, then retry.',\n * 400,\n * );\n * }\n * ```\n */\nexport class TapServerError extends Error {\n readonly code: string;\n readonly status: number | undefined;\n\n constructor(code: string, message: string, status?: number) {\n super(message);\n this.name = 'TapServerError';\n this.code = code;\n this.status = status;\n }\n\n /** The wire body the handler answers with, without the status. */\n toJSON(): { error: { code: string; message: string } } {\n return { error: { code: this.code, message: this.message } };\n }\n\n /**\n * Narrows an unknown value to a `TapServerError` by name and by carrying a\n * string `code`, so it holds across module instances.\n */\n static is(value: unknown): value is TapServerError {\n return (\n value instanceof Error &&\n value.name === 'TapServerError' &&\n 'code' in value &&\n typeof value.code === 'string'\n );\n }\n}\n"]}
@@ -1,20 +1,24 @@
1
1
  "use strict";Object.defineProperty(exports, "__esModule", {value: true});
2
2
 
3
- var _chunk6HETNZJYcjs = require('./chunk-6HETNZJY.cjs');
3
+ var _chunk2DBSVKAUcjs = require('./chunk-2DBSVKAU.cjs');
4
4
 
5
5
  // src/testing/environment.ts
6
6
  var HOST_THEME = {
7
- "--bg-paper": "#fdfdfd",
8
- "--bg-canvas": "#f2f3ee",
9
- "--bg-light-gray": "#edf1f5",
10
- "--fg-primary": "#121212",
11
- "--fg-sub": "rgba(18, 18, 18, 0.7)",
12
- "--divider": "#d5d7da",
13
- "--lime": "#b4d773",
14
- "--deep-green": "#234e20",
15
- "--danger": "#b42318",
16
- "--on-accent": "#ffffff",
17
- "--font-one-ui": "'Inter Variable', 'Inter', system-ui, sans-serif"
7
+ "--bg-base": "#ffffff",
8
+ "--bg-subtle": "rgba(18, 18, 18, 0.04)",
9
+ "--bg-raised": "#ffffff",
10
+ "--fg-default": "#121212",
11
+ "--fg-muted": "rgba(18, 18, 18, 0.6)",
12
+ "--fg-inverse": "#ffffff",
13
+ "--tap-accent": "#4f46e5",
14
+ "--tap-accent-fg": "#ffffff",
15
+ "--tap-danger": "#b42318",
16
+ "--tap-scrim": "rgba(18, 18, 18, 0.7)",
17
+ "--tap-radius": "6px",
18
+ "--tap-space": "8px",
19
+ "--font-sans": "system-ui, -apple-system, sans-serif",
20
+ "--font-mono": "ui-monospace, SFMono-Regular, monospace",
21
+ "--divider": "rgba(18, 18, 18, 0.12)"
18
22
  };
19
23
  function installHostTheme(root) {
20
24
  for (const [name, value] of Object.entries(HOST_THEME)) {
@@ -31,7 +35,7 @@ function isMock(value) {
31
35
  }
32
36
  function requireMock(value, matcher) {
33
37
  if (isMock(value)) return value;
34
- throw new (0, _chunk6HETNZJYcjs.TapError)(
38
+ throw new (0, _chunk2DBSVKAUcjs.TapError)(
35
39
  "TAP_TEST_TARGET_INVALID",
36
40
  `${matcher}() takes the mock renderWithTap() returned. Call it as expect(mock).${matcher}(\u2026), not on the component or the tap object.`
37
41
  );
@@ -135,4 +139,4 @@ function registerTapMatchers(target) {
135
139
 
136
140
 
137
141
  exports.installHostTheme = installHostTheme; exports.clearTapPortals = clearTapPortals; exports.tapMatchers = tapMatchers; exports.registerTapMatchers = registerTapMatchers;
138
- //# sourceMappingURL=chunk-O357OJXL.cjs.map
142
+ //# sourceMappingURL=chunk-3YWVYA42.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["/home/runner/work/extension-sdk/extension-sdk/dist/chunk-3YWVYA42.cjs","../src/testing/environment.ts","../src/testing/matchers.ts"],"names":[],"mappings":"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","file":"/home/runner/work/extension-sdk/extension-sdk/dist/chunk-3YWVYA42.cjs","sourcesContent":[null,"const HOST_THEME: Record<string, string> = {\n '--bg-base': '#ffffff',\n '--bg-subtle': 'rgba(18, 18, 18, 0.04)',\n '--bg-raised': '#ffffff',\n '--fg-default': '#121212',\n '--fg-muted': 'rgba(18, 18, 18, 0.6)',\n '--fg-inverse': '#ffffff',\n '--tap-accent': '#4f46e5',\n '--tap-accent-fg': '#ffffff',\n '--tap-danger': '#b42318',\n '--tap-scrim': 'rgba(18, 18, 18, 0.7)',\n '--tap-radius': '6px',\n '--tap-space': '8px',\n '--font-sans': 'system-ui, -apple-system, sans-serif',\n '--font-mono': 'ui-monospace, SFMono-Regular, monospace',\n '--divider': 'rgba(18, 18, 18, 0.12)',\n};\n\n/**\n * Sets the host's fifteen CSS variables on an element, so a component renders in\n * a test the way it renders in the host.\n *\n * @remarks\n * Sets the light values as inline custom properties, which inherit into everything\n * below the element. The vitest and jest presets call it on\n * `document.documentElement` for you, so call it yourself only in a test that\n * renders outside `renderWithTap` or wants the variables on one subtree. It sets\n * no dark values; drive the mode through `renderWithTap`'s `theme` option, which\n * is what `useTapTheme` reads.\n *\n * @example\n * ```ts\n * import { installHostTheme } from '@tappify/extension-sdk/testing';\n *\n * function mountRoot(): HTMLElement {\n * const root = document.createElement('div');\n * installHostTheme(root);\n * document.body.append(root);\n * return root;\n * }\n * ```\n */\nexport function installHostTheme(root: HTMLElement): void {\n for (const [name, value] of Object.entries(HOST_THEME)) {\n root.style.setProperty(name, value);\n }\n}\n\nexport function clearTapPortals(root: ParentNode = document): void {\n for (const node of root.querySelectorAll('[data-tap-portal]')) node.remove();\n}\n","import { TapError } from '../client/errors';\nimport type { TapMock } from './mock';\n\n/** What each matcher returns, in the shape vitest and jest both read. */\nexport interface MatcherResult {\n pass: boolean;\n /** Built only when the runner reports, and worded for both `expect` and `not`. */\n message(): string;\n}\n\n/** The part of a runner's `expect` that `registerTapMatchers` needs. */\nexport interface MatcherTarget {\n /** Both runners take a map of matcher functions keyed by matcher name. */\n extend(matchers: Record<string, unknown>): void;\n}\n\nfunction isMock(value: unknown): value is TapMock {\n return (\n typeof value === 'object' &&\n value !== null &&\n 'calls' in value &&\n 'documents' in value &&\n typeof value.documents === 'function'\n );\n}\n\nfunction requireMock(value: unknown, matcher: string): TapMock {\n if (isMock(value)) return value;\n throw new TapError(\n 'TAP_TEST_TARGET_INVALID',\n `${matcher}() takes the mock renderWithTap() returned. Call it as expect(mock).${matcher}(…), not on the component or the tap object.`,\n );\n}\n\nfunction tagOf(value: unknown): string {\n return Object.prototype.toString.call(value);\n}\n\nfunction sameList(left: unknown[], right: unknown[]): boolean {\n return (\n left.length === right.length &&\n left.every((item, index) => same(item, right[index]))\n );\n}\n\nfunction bytesOf(view: ArrayBufferView): number[] {\n return [...new Uint8Array(view.buffer, view.byteOffset, view.byteLength)];\n}\n\nfunction same(left: unknown, right: unknown): boolean {\n if (Object.is(left, right)) return true;\n\n const tag = tagOf(left);\n if (tag !== tagOf(right)) return false;\n\n if (\n typeof left !== 'object' ||\n typeof right !== 'object' ||\n left === null ||\n right === null\n ) {\n return false;\n }\n\n if (left instanceof Date && right instanceof Date) {\n return left.valueOf() === right.valueOf();\n }\n if (left instanceof RegExp && right instanceof RegExp) {\n return left.source === right.source && left.flags === right.flags;\n }\n if (left instanceof Map && right instanceof Map) {\n return sameList([...left.entries()], [...right.entries()]);\n }\n if (left instanceof Set && right instanceof Set) {\n return sameList([...left.values()], [...right.values()]);\n }\n if (ArrayBuffer.isView(left) && ArrayBuffer.isView(right)) {\n return sameList(bytesOf(left), bytesOf(right));\n }\n if (Array.isArray(left) && Array.isArray(right)) {\n return sameList(left, right);\n }\n if (tag !== '[object Object]') return false;\n\n const keys = Object.keys(left);\n return (\n keys.length === Object.keys(right).length &&\n keys.every(\n key =>\n key in right && same(Reflect.get(left, key), Reflect.get(right, key)),\n )\n );\n}\n\n/**\n * The four matchers that assert on what a component asked the host to do.\n *\n * @remarks\n * Each takes the mock `renderWithTap` returns, so the assertion is\n * `expect(mock).toHaveToasted(…)` rather than `expect(component)`; anything else\n * throws a `TapError` carrying `TAP_TEST_TARGET_INVALID`. A failure names every\n * call that was recorded, so a wrong path or message reads without a debugger. The\n * vitest and jest presets register these, and both declare the four on the runner's\n * own `Matchers` interface, so `expect(mock).toHaveToasted` typechecks once the\n * preset is in the setup files.\n *\n * @example\n * ```ts\n * import '@tappify/extension-sdk/testing/vitest';\n * import { createTapMock } from '@tappify/extension-sdk/testing';\n * import { expect, it } from 'vitest';\n *\n * it('toasts when a release ships', () => {\n * const mock = createTapMock();\n * mock.tap.ui.toast('A release shipped', 'neutral');\n * expect(mock).toHaveToasted('A release shipped');\n * });\n * ```\n */\nexport const tapMatchers = {\n /** Passes when `tap.nav.push` was called with exactly that path. */\n toHaveNavigatedTo(received: unknown, path: string): MatcherResult {\n const mock = requireMock(received, 'toHaveNavigatedTo');\n const pass = mock.calls.navigations.includes(path);\n return {\n pass,\n message: () =>\n pass\n ? `Expected no navigation to ${path}, but tap.nav.push was called with it.`\n : `Expected a navigation to ${path}. tap.nav.push was called with: ${\n mock.calls.navigations.length === 0\n ? 'nothing'\n : mock.calls.navigations.join(', ')\n }.`,\n };\n },\n\n /** Passes when a toast matched the string exactly, or the pattern anywhere. */\n toHaveToasted(received: unknown, expected: string | RegExp): MatcherResult {\n const mock = requireMock(received, 'toHaveToasted');\n const messages = mock.calls.toasts.map(toast => toast.message);\n const pass = messages.some(message =>\n typeof expected === 'string'\n ? message === expected\n : expected.test(message),\n );\n return {\n pass,\n message: () =>\n pass\n ? `Expected no toast matching ${String(expected)}, but one was shown.`\n : `Expected a toast matching ${String(expected)}. Toasts shown: ${\n messages.length === 0 ? 'none' : messages.join(' | ')\n }.`,\n };\n },\n\n /**\n * Passes when `tap.actions.run` was called with that action, and with input\n * deep-equal to `input` when one is given.\n */\n toHaveRunAction(\n received: unknown,\n actionId: string,\n input?: unknown,\n ): MatcherResult {\n const mock = requireMock(received, 'toHaveRunAction');\n const runs = mock.calls.actions.filter(run => run.actionId === actionId);\n const pass =\n input === undefined\n ? runs.length > 0\n : runs.some(run => same(run.input, input));\n return {\n pass,\n message: () =>\n pass\n ? `Expected tap.actions.run(\"${actionId}\") not to be called.`\n : `Expected tap.actions.run(\"${actionId}\"${\n input === undefined ? '' : `, ${JSON.stringify(input)}`\n }). Runs recorded: ${\n mock.calls.actions.length === 0\n ? 'none'\n : JSON.stringify(mock.calls.actions)\n }.`,\n };\n },\n\n /**\n * Passes when the mock's storage holds a document deep-equal to that one in\n * that collection.\n */\n toHaveStored(\n received: unknown,\n collection: string,\n document: unknown,\n ): MatcherResult {\n const mock = requireMock(received, 'toHaveStored');\n const stored = mock.documents(collection);\n const pass = stored.some(candidate => same(candidate, document));\n return {\n pass,\n message: () =>\n pass\n ? `Expected \"${collection}\" not to hold ${JSON.stringify(document)}.`\n : `Expected \"${collection}\" to hold ${JSON.stringify(document)}. It holds: ${\n stored.length === 0 ? 'nothing' : JSON.stringify(stored)\n }.`,\n };\n },\n};\n\n/**\n * Registers the four matchers on a runner's `expect`.\n *\n * @remarks\n * The vitest and jest presets call this, so a suite that loads\n * `@tappify/extension-sdk/testing/vitest` or `.../testing/jest` as a setup file\n * needs nothing else. Call it yourself only in a suite that does not use a preset,\n * and once — registering twice replaces the same four names. It adds no types of\n * its own, so a suite without a preset declares the four on the runner's `Matchers`\n * interface itself.\n *\n * @example\n * ```ts\n * import { registerTapMatchers } from '@tappify/extension-sdk/testing';\n * import { expect } from 'vitest';\n *\n * registerTapMatchers(expect);\n * ```\n */\nexport function registerTapMatchers(target: MatcherTarget): void {\n target.extend(tapMatchers);\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 _chunkPRCZMOLDcjs = require('./chunk-PRCZMOLD.cjs');
3
+ var _chunkOPPYEA7Fcjs = require('./chunk-OPPYEA7F.cjs');
4
4
 
5
5
 
6
- var _chunk6HETNZJYcjs = require('./chunk-6HETNZJY.cjs');
6
+ var _chunk2DBSVKAUcjs = require('./chunk-2DBSVKAU.cjs');
7
7
 
8
8
  // src/client/hooks.ts
9
9
  var _react = require('react');
10
10
  function useTapSlice(key, select) {
11
- const tap = _chunkPRCZMOLDcjs.useTap.call(void 0, );
11
+ const tap = _chunkOPPYEA7Fcjs.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 = _chunkPRCZMOLDcjs.useTap.call(void 0, );
48
+ const tap = _chunkOPPYEA7Fcjs.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, _chunk6HETNZJYcjs.TapError)(
111
+ throw new (0, _chunk2DBSVKAUcjs.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 = _chunkPRCZMOLDcjs.useTap.call(void 0, );
118
+ const tap = _chunkOPPYEA7Fcjs.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 = _chunkPRCZMOLDcjs.useTap.call(void 0, );
142
+ const tap = _chunkOPPYEA7Fcjs.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, _chunk6HETNZJYcjs.TapError)(
153
+ throw new (0, _chunk2DBSVKAUcjs.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, _chunk6HETNZJYcjs.TapError)(
182
+ throw new (0, _chunk2DBSVKAUcjs.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 = _chunkPRCZMOLDcjs.useTap.call(void 0, );
188
+ const tap = _chunkOPPYEA7Fcjs.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-GAUZYBXE.cjs.map
250
+ //# sourceMappingURL=chunk-5AAPD5WV.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["/home/runner/work/extension-sdk/extension-sdk/dist/chunk-5AAPD5WV.cjs","../src/client/hooks.ts","../src/client/query.ts"],"names":["useCallback"],"mappings":"AAAA;AACE;AACF,wDAA6B;AAC7B;AACE;AACF,wDAA6B;AAC7B;AACA;ACPA,8BAAkD;AAIlD,SAAS,WAAA,CAAe,GAAA,EAAmB,MAAA,EAA4B;AACrE,EAAA,MAAM,IAAA,EAAM,sCAAA,CAAO;AAEnB,EAAA,MAAM,UAAA,EAAY,gCAAA;AAAA,IAChB,CAAC,QAAA,EAAA,GAAyB,GAAA,CAAI,WAAA,CAAY,GAAA,EAAK,QAAQ,CAAA;AAAA,IACvD,CAAC,GAAA,EAAK,GAAG;AAAA,EACX,CAAA;AACA,EAAA,MAAM,SAAA,EAAW,gCAAA,CAAY,EAAA,GAAM,MAAA,CAAO,GAAG,CAAA,EAAG,CAAC,GAAA,EAAK,MAAM,CAAC,CAAA;AAE7D,EAAA,OAAO,yCAAA,SAAqB,EAAW,QAAA,EAAU,QAAQ,CAAA;AAC3D;AAEA,IAAM,WAAA,EAAa,CAAC,GAAA,EAAA,GAA0B,GAAA,CAAI,IAAA;AAClD,IAAM,cAAA,EAAgB,CAAC,GAAA,EAAA,GAA6B,GAAA,CAAI,OAAA;AACxD,IAAM,cAAA,EAAgB,CAAC,GAAA,EAAA,GAAyB,GAAA,CAAI,OAAA;AACpD,IAAM,YAAA,EAAc,CAAC,GAAA,EAAA,GAA2B,GAAA,CAAI,KAAA;AACpD,IAAM,WAAA,EAAa,CAAC,GAAA,EAAA,GAAsB,GAAA,CAAI,EAAA,CAAG,IAAA;AACjD,IAAM,aAAA,EAAe,CAAC,GAAA,EAAA,GAAqC,GAAA,CAAI,MAAA;AAC/D,IAAM,cAAA,EAAgB,CAAC,GAAA,EAAA,GAAsC,GAAA,CAAI,OAAA;AAwB1D,SAAS,UAAA,CAAA,EAA0B;AACxC,EAAA,OAAO,WAAA,CAAY,MAAA,EAAQ,UAAU,CAAA;AACvC;AAqBO,SAAS,aAAA,CAAA,EAAgC;AAC9C,EAAA,OAAO,WAAA,CAAY,SAAA,EAAW,aAAa,CAAA;AAC7C;AA0BO,SAAS,aAAA,CAAA,EAA4B;AAC1C,EAAA,OAAO,WAAA,CAAY,SAAA,EAAW,aAAa,CAAA;AAC7C;AAsBO,SAAS,WAAA,CAAA,EAA4B;AAC1C,EAAA,OAAO,WAAA,CAAY,OAAA,EAAS,WAAW,CAAA;AACzC;AA8BO,SAAS,UAAA,CAAA,EAAsB;AACpC,EAAA,OAAO,WAAA,CAAY,MAAA,EAAQ,UAAU,CAAA;AACvC;AAuBO,SAAS,YAAA,CAAA,EAAuC;AACrD,EAAA,OAAO,WAAA,CAAY,QAAA,EAAU,YAAY,CAAA;AAC3C;AAsBO,SAAS,aAAA,CAAA,EAAyC;AACvD,EAAA,OAAO,WAAA,CAAY,SAAA,EAAW,aAAa,CAAA;AAC7C;AA4BO,SAAS,WAAA,CACd,GAAA,EACqC;AACrC,EAAA,MAAM,IAAA,EAAM,sCAAA,CAAO;AAEnB,EAAA,MAAM,UAAA,EAAY,gCAAA;AAAA,IAChB,CAAC,QAAA,EAAA,GAAyB,GAAA,CAAI,WAAA,CAAY,CAAA,MAAA,EAAS,GAAG,CAAA,CAAA;AAC7C,IAAA;AACX,EAAA;AAC6C,EAAA;AACjC,EAAA;AACI,IAAA;AACY,MAAA;AAC1B,IAAA;AACS,IAAA;AACX,EAAA;AAEmC,EAAA;AACjB,EAAA;AACpB;AD7L+C;AACA;AE/D/C;AACE;AACA;AAEK;AACe;AAkBA;AAuDpB;AAEyC,EAAA;AAC3C;AAwBE;AAGwC,EAAA;AAC1C;AAkBE;AAEqC,EAAA;AACvC;AAe4D;AAClC,EAAA;AAC1B;AAgB4E;AACpC,EAAA;AACxC;AAKE;AAG+B,EAAA;AACD,EAAA;AAEd,EAAA;AACM,IAAA;AACH,IAAA;AAClB,EAAA;AAE0C,EAAA;AAE3B,EAAA;AACsB,IAAA;AACZ,IAAA;AAEG,IAAA;AACM,MAAA;AACG,QAAA;AACjC,MAAA;AACH,IAAA;AAEa,IAAA;AAC6B,MAAA;AAC1C,IAAA;AAC8B,EAAA;AAClC;AAE2C;AACrC,EAAA;AACoB,IAAA;AAChB,EAAA;AACI,IAAA;AACR,MAAA;AACA,MAAA;AACF,IAAA;AACF,EAAA;AACF;AAuDgC;AACX,EAAA;AACe,EAAA;AACJ,EAAA;AACc,EAAA;AAEpB,EAAA;AACtB,IAAA;AACmC,IAAA;AACP,IAAA;AACjB,IAAA;AACJ,IAAA;AACR,EAAA;AAEsC,EAAA;AAEV,EAAA;AACK,EAAA;AACb,IAAA;AACH,EAAA;AAEX,EAAA;AACQ,IAAA;AACC,IAAA;AACI,IAAA;AAClB,IAAA;AACF,EAAA;AACF;AAoC8B;AAET,EAAA;AACe,EAAA;AACJ,EAAA;AACb,EAAA;AACD,IAAA;AACd,IAAA;AACA,IAAA;AACA,IAAA;AACF,EAAA;AACiC,EAAA;AAEI,EAAA;AACzB,IAAA;AACR,MAAA;AACkB,MAAA;AACpB,IAAA;AACF,EAAA;AAEwB,EAAA;AACtB,IAAA;AAC8B,IAAA;AACF,IAAA;AACjB,IAAA;AACJ,IAAA;AACR,EAAA;AAEsC,EAAA;AAEV,EAAA;AACK,EAAA;AACb,IAAA;AACH,EAAA;AAEX,EAAA;AACQ,IAAA;AACC,IAAA;AACI,IAAA;AAClB,IAAA;AACF,EAAA;AACF;AAEwE;AAGpE,EAAA;AAQJ;AAE8E;AACnC,EAAA;AAE/B,EAAA;AACR,IAAA;AAC4B,IAAA;AAC9B,EAAA;AACF;AAkC6C;AAGxB,EAAA;AACe,EAAA;AACG,EAAA;AACY,EAAA;AAEzB,EAAA;AACtB,IAAA;AAC0B,IAAA;AACf,IAAA;AACJ,IAAA;AACR,EAAA;AAE+B,EAAA;AACK,EAAA;AACL,IAAA;AACS,MAAA;AACtC,IAAA;AAC+B,EAAA;AAElC,EAAA;AAC6C,IAAA;AACf,IAAA;AAC9B,EAAA;AAEaA,EAAAA;AACmB,IAAA;AACH,MAAA;AAC3B,IAAA;AACO,IAAA;AACT,EAAA;AAEcA,EAAAA;AAC8C,IAAA;AACnD,IAAA;AACT,EAAA;AAE6B,EAAA;AACK,EAAA;AACb,IAAA;AACH,EAAA;AAEX,EAAA;AACQ,IAAA;AACC,IAAA;AACI,IAAA;AAClB,IAAA;AACA,IAAA;AACA,IAAA;AACF,EAAA;AACF;AFrP+C;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA","file":"/home/runner/work/extension-sdk/extension-sdk/dist/chunk-5AAPD5WV.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 { TapEmptyState, useTapAuth } from '@tappify/extension-sdk';\n *\n * function RevenuePanel() {\n * const auth = useTapAuth();\n * if (!auth.can('revenue:read')) {\n * return <TapEmptyState 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 { TapTable, useTapSize } from '@tappify/extension-sdk';\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 * <TapTable\n * columns={[{ key: 'term', label: 'Term' }]}\n * rows={shown}\n * rowKey={row => row.term}\n * />\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 { TapButton, useTapState } from '@tappify/extension-sdk';\n *\n * function StepPicker() {\n * const [step, setStep] = useTapState<string>('selectedStep');\n * return (\n * <TapButton onClick={() => setStep('install')}>\n * {step ?? 'Pick a step'}\n * </TapButton>\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 { TapStat, useTapFilters, useTapQuery } from '@tappify/extension-sdk';\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 <TapStat label=\"Downloads\" value={total} />;\n * }\n * ```\n *\n * @example\n * ```tsx\n * import { TapTable, useTapQuery } from '@tappify/extension-sdk';\n *\n * function Keywords() {\n * const keywords = useTapQuery({ kind: 'keywords' });\n * return (\n * <TapTable\n * columns={[\n * { key: 'term', label: 'Term' },\n * { key: 'position', label: 'Rank', align: 'end' },\n * ]}\n * rows={keywords.data?.keywords ?? []}\n * rowKey={keyword => keyword.id}\n * />\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 { TapButton, TapErrorState, useTapFilters, useTapServer } from '@tappify/extension-sdk';\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 <TapErrorState title=\"The summary did not load\" description={summary.error.message} />;\n * }\n * return <TapButton loading={summary.isLoading} onClick={summary.refetch}>Refresh</TapButton>;\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 { TapButton, useTapStorage } from '@tappify/extension-sdk';\n *\n * function CompactToggle() {\n * const preferences = useTapStorage('preferences');\n * return (\n * <TapButton\n * loading={preferences.isLoading}\n * onClick={() => void preferences.patch({ compact: true })}\n * >\n * Compact\n * </TapButton>\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"]}
@@ -1,9 +1,9 @@
1
1
  import {
2
2
  useTap
3
- } from "./chunk-QA5AL3VZ.mjs";
3
+ } from "./chunk-QHM7XNRK.mjs";
4
4
  import {
5
5
  TapError
6
- } from "./chunk-QXD7WVKK.mjs";
6
+ } from "./chunk-CSTYBKZC.mjs";
7
7
 
8
8
  // src/client/hooks.ts
9
9
  import { useCallback, useSyncExternalStore } from "react";
@@ -247,4 +247,4 @@ export {
247
247
  useTapServer,
248
248
  useTapStorage
249
249
  };
250
- //# sourceMappingURL=chunk-SGEYDV3K.mjs.map
250
+ //# sourceMappingURL=chunk-BDCX6LRQ.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/client/hooks.ts","../src/client/query.ts"],"sourcesContent":["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 { TapEmptyState, useTapAuth } from '@tappify/extension-sdk';\n *\n * function RevenuePanel() {\n * const auth = useTapAuth();\n * if (!auth.can('revenue:read')) {\n * return <TapEmptyState 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 { TapTable, useTapSize } from '@tappify/extension-sdk';\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 * <TapTable\n * columns={[{ key: 'term', label: 'Term' }]}\n * rows={shown}\n * rowKey={row => row.term}\n * />\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 { TapButton, useTapState } from '@tappify/extension-sdk';\n *\n * function StepPicker() {\n * const [step, setStep] = useTapState<string>('selectedStep');\n * return (\n * <TapButton onClick={() => setStep('install')}>\n * {step ?? 'Pick a step'}\n * </TapButton>\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 { TapStat, useTapFilters, useTapQuery } from '@tappify/extension-sdk';\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 <TapStat label=\"Downloads\" value={total} />;\n * }\n * ```\n *\n * @example\n * ```tsx\n * import { TapTable, useTapQuery } from '@tappify/extension-sdk';\n *\n * function Keywords() {\n * const keywords = useTapQuery({ kind: 'keywords' });\n * return (\n * <TapTable\n * columns={[\n * { key: 'term', label: 'Term' },\n * { key: 'position', label: 'Rank', align: 'end' },\n * ]}\n * rows={keywords.data?.keywords ?? []}\n * rowKey={keyword => keyword.id}\n * />\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 { TapButton, TapErrorState, useTapFilters, useTapServer } from '@tappify/extension-sdk';\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 <TapErrorState title=\"The summary did not load\" description={summary.error.message} />;\n * }\n * return <TapButton loading={summary.isLoading} onClick={summary.refetch}>Refresh</TapButton>;\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 { TapButton, useTapStorage } from '@tappify/extension-sdk';\n *\n * function CompactToggle() {\n * const preferences = useTapStorage('preferences');\n * return (\n * <TapButton\n * loading={preferences.isLoading}\n * onClick={() => void preferences.patch({ compact: true })}\n * >\n * Compact\n * </TapButton>\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"],"mappings":"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","names":["value","useCallback","useCallback"]}
@@ -1,10 +1,10 @@
1
1
  import {
2
2
  EVENTS
3
- } from "./chunk-SGCYEEXD.mjs";
3
+ } from "./chunk-MNDVYPEP.mjs";
4
4
  import {
5
5
  TapError,
6
6
  TapServerError
7
- } from "./chunk-QXD7WVKK.mjs";
7
+ } from "./chunk-CSTYBKZC.mjs";
8
8
 
9
9
  // src/server/codes.ts
10
10
  var SERVER_ERROR_CODES = {
@@ -124,15 +124,10 @@ function readText(value, key) {
124
124
  // src/server/handler.ts
125
125
  var MAX_BODY_BYTES = 1048576;
126
126
  var WORK_OPERATIONS = [
127
- "configure",
128
127
  "list_containers",
129
- "describe_container",
130
128
  "create",
131
129
  "attach",
132
130
  "comment",
133
- "set_status",
134
- "hydrate_event",
135
- "disconnect",
136
131
  "status",
137
132
  "resolve"
138
133
  ];
@@ -243,17 +238,10 @@ function resolveRoute(options, method, segments, url, payload) {
243
238
  }
244
239
  if (head === "work" && method === "POST" && name !== void 0) {
245
240
  const operation = WORK_OPERATIONS.find((candidate) => candidate === name);
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);
241
+ return route(
242
+ operation === void 0 ? void 0 : options.work?.[operation],
243
+ bodyInput
244
+ );
257
245
  }
258
246
  if (head === "events" && method === "POST") {
259
247
  const envelope = readRecord(payload, "event");
@@ -370,9 +358,8 @@ export {
370
358
  verifyTappifyToken,
371
359
  INTERNAL_MESSAGE,
372
360
  wireError,
373
- readUnknown,
374
361
  readRecord,
375
362
  readText,
376
363
  createTappifyHandler
377
364
  };
378
- //# sourceMappingURL=chunk-KZ5NGFUP.mjs.map
365
+ //# sourceMappingURL=chunk-C3YNVHBP.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 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"],"mappings":"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","names":[]}
@@ -62,4 +62,4 @@ export {
62
62
  TapError,
63
63
  TapServerError
64
64
  };
65
- //# sourceMappingURL=chunk-QXD7WVKK.mjs.map
65
+ //# sourceMappingURL=chunk-CSTYBKZC.mjs.map