@tappify/extension-sdk 2.0.0 → 2.1.0-beta.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +208 -0
- package/dist/chunk-2CY5HVY7.mjs +59 -0
- package/dist/chunk-2CY5HVY7.mjs.map +1 -0
- package/dist/{chunk-2DBSVKAU.cjs → chunk-6HETNZJY.cjs} +1 -1
- package/dist/chunk-6HETNZJY.cjs.map +1 -0
- package/dist/{chunk-PYJMJF44.cjs → chunk-7YKH4LZN.cjs} +9 -2
- package/dist/chunk-7YKH4LZN.cjs.map +1 -0
- package/dist/{chunk-RMUWRESK.cjs → chunk-A74HCO6T.cjs} +31 -18
- package/dist/chunk-A74HCO6T.cjs.map +1 -0
- package/dist/chunk-BPCGXXMD.cjs +781 -0
- package/dist/chunk-BPCGXXMD.cjs.map +1 -0
- package/dist/chunk-EB5XWQAU.cjs +132 -0
- package/dist/chunk-EB5XWQAU.cjs.map +1 -0
- package/dist/{chunk-5AAPD5WV.cjs → chunk-GAUZYBXE.cjs} +11 -11
- package/dist/chunk-GAUZYBXE.cjs.map +1 -0
- package/dist/chunk-J5LGTIGS.mjs +10 -0
- package/dist/chunk-J5LGTIGS.mjs.map +1 -0
- package/dist/{chunk-ZT35X7KL.mjs → chunk-KZ5NGFUP.mjs} +22 -9
- package/dist/chunk-KZ5NGFUP.mjs.map +1 -0
- package/dist/chunk-NN263PGQ.cjs +70 -0
- package/dist/chunk-NN263PGQ.cjs.map +1 -0
- package/dist/{chunk-3YWVYA42.cjs → chunk-O357OJXL.cjs} +14 -18
- package/dist/chunk-O357OJXL.cjs.map +1 -0
- package/dist/chunk-ONJMPO3P.cjs +59 -0
- package/dist/chunk-ONJMPO3P.cjs.map +1 -0
- package/dist/{chunk-OPPYEA7F.cjs → chunk-PRCZMOLD.cjs} +3 -3
- package/dist/chunk-PRCZMOLD.cjs.map +1 -0
- package/dist/chunk-Q7SFCCGT.cjs +10 -0
- package/dist/chunk-Q7SFCCGT.cjs.map +1 -0
- package/dist/{chunk-QHM7XNRK.mjs → chunk-QA5AL3VZ.mjs} +2 -2
- package/dist/{chunk-QHM7XNRK.mjs.map → chunk-QA5AL3VZ.mjs.map} +1 -1
- package/dist/chunk-QJMZRV7A.mjs +132 -0
- package/dist/chunk-QJMZRV7A.mjs.map +1 -0
- package/dist/{chunk-CSTYBKZC.mjs → chunk-QXD7WVKK.mjs} +1 -1
- package/dist/chunk-QXD7WVKK.mjs.map +1 -0
- package/dist/{chunk-4C5Z3CIK.mjs → chunk-RBIQT75L.mjs} +2 -2
- package/dist/chunk-RBIQT75L.mjs.map +1 -0
- package/dist/{chunk-MNDVYPEP.mjs → chunk-SGCYEEXD.mjs} +9 -2
- package/dist/chunk-SGCYEEXD.mjs.map +1 -0
- package/dist/{chunk-BDCX6LRQ.mjs → chunk-SGEYDV3K.mjs} +3 -3
- package/dist/chunk-SGEYDV3K.mjs.map +1 -0
- package/dist/{chunk-IGK2AP6J.mjs → chunk-SLGOHIBL.mjs} +13 -17
- package/dist/chunk-SLGOHIBL.mjs.map +1 -0
- package/dist/chunk-SWG7CNTZ.mjs +781 -0
- package/dist/chunk-SWG7CNTZ.mjs.map +1 -0
- package/dist/{chunk-7KIKOH5H.cjs → chunk-VV7ELNRW.cjs} +3 -3
- package/dist/chunk-VV7ELNRW.cjs.map +1 -0
- package/dist/chunk-Z43DPNZ6.mjs +70 -0
- package/dist/chunk-Z43DPNZ6.mjs.map +1 -0
- package/dist/client-BO3kavn0.d.ts +25 -0
- package/dist/client-DE-vMgdO.d.cts +25 -0
- package/dist/codegen.cjs.map +1 -1
- package/dist/codegen.d.cts +43 -7
- package/dist/codegen.d.ts +43 -7
- package/dist/codegen.mjs.map +1 -1
- package/dist/{constants-DJ_KgZpN.d.cts → constants-Bh759FUI.d.cts} +44 -8
- package/dist/{constants-DJ_KgZpN.d.ts → constants-Bh759FUI.d.ts} +44 -8
- package/dist/{context-Dt6xh_5t.d.cts → context-DI0EJCRD.d.cts} +5 -4
- package/dist/{context-Ml7k_V14.d.ts → context-mr8R5CNt.d.ts} +5 -4
- package/dist/{errors-COsBh7kd.d.cts → errors-Cqh0wrqt.d.cts} +3 -2
- package/dist/{errors-COsBh7kd.d.ts → errors-Cqh0wrqt.d.ts} +3 -2
- package/dist/host.cjs +5 -4
- package/dist/host.cjs.map +1 -1
- package/dist/host.d.cts +5 -5
- package/dist/host.d.ts +5 -5
- package/dist/host.mjs +4 -3
- package/dist/host.mjs.map +1 -1
- package/dist/index.cjs +9 -656
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +69 -627
- package/dist/index.d.ts +69 -627
- package/dist/index.mjs +5 -652
- package/dist/index.mjs.map +1 -1
- package/dist/manifest.cjs +142 -86
- package/dist/manifest.cjs.map +1 -1
- package/dist/manifest.d.cts +97 -12
- package/dist/manifest.d.ts +97 -12
- package/dist/manifest.mjs +129 -73
- package/dist/manifest.mjs.map +1 -1
- package/dist/server.cjs +15 -66
- package/dist/server.cjs.map +1 -1
- package/dist/server.d.cts +6 -6
- package/dist/server.d.ts +6 -6
- package/dist/server.mjs +10 -61
- package/dist/server.mjs.map +1 -1
- package/dist/testing/index.cjs +73 -750
- package/dist/testing/index.cjs.map +1 -1
- package/dist/testing/index.d.cts +30 -11
- package/dist/testing/index.d.ts +30 -11
- package/dist/testing/index.mjs +89 -766
- package/dist/testing/index.mjs.map +1 -1
- package/dist/testing/jest.cjs +6 -5
- package/dist/testing/jest.cjs.map +1 -1
- package/dist/testing/jest.mjs +3 -2
- package/dist/testing/jest.mjs.map +1 -1
- package/dist/testing/vitest.cjs +6 -5
- package/dist/testing/vitest.cjs.map +1 -1
- package/dist/testing/vitest.d.cts +10 -0
- package/dist/testing/vitest.d.ts +10 -0
- package/dist/testing/vitest.mjs +3 -2
- package/dist/testing/vitest.mjs.map +1 -1
- package/dist/{types-DDAGLpov.d.ts → types-BdDaLnqs.d.cts} +4 -3
- package/dist/{types-DrpQSv_2.d.cts → types-uGHrRaOj.d.ts} +4 -3
- package/dist/ui/styles.css +740 -0
- package/dist/ui.cjs +1018 -0
- package/dist/ui.cjs.map +1 -0
- package/dist/ui.d.cts +231 -0
- package/dist/ui.d.ts +231 -0
- package/dist/ui.mjs +1018 -0
- package/dist/ui.mjs.map +1 -0
- package/dist/vite-preview-bootstrap.cjs +66 -0
- package/dist/vite-preview-bootstrap.cjs.map +1 -0
- package/dist/vite-preview-bootstrap.d.cts +15 -0
- package/dist/vite-preview-bootstrap.d.ts +15 -0
- package/dist/vite-preview-bootstrap.mjs +66 -0
- package/dist/vite-preview-bootstrap.mjs.map +1 -0
- package/dist/vite-preview.cjs +876 -0
- package/dist/vite-preview.cjs.map +1 -0
- package/dist/vite-preview.d.cts +9 -0
- package/dist/vite-preview.d.ts +9 -0
- package/dist/vite-preview.mjs +876 -0
- package/dist/vite-preview.mjs.map +1 -0
- package/dist/vite.cjs +320 -60
- package/dist/vite.cjs.map +1 -1
- package/dist/vite.d.cts +54 -8
- package/dist/vite.d.ts +54 -8
- package/dist/vite.mjs +312 -54
- package/dist/vite.mjs.map +1 -1
- package/dist/work-events-BlOVpK0k.d.ts +547 -0
- package/dist/work-events-D97EoqvS.d.cts +547 -0
- package/package.json +19 -2
- package/schema/extension-v2.json +440 -121
- package/dist/chunk-2DBSVKAU.cjs.map +0 -1
- package/dist/chunk-3YWVYA42.cjs.map +0 -1
- package/dist/chunk-4C5Z3CIK.mjs.map +0 -1
- package/dist/chunk-5AAPD5WV.cjs.map +0 -1
- package/dist/chunk-7KIKOH5H.cjs.map +0 -1
- package/dist/chunk-BDCX6LRQ.mjs.map +0 -1
- package/dist/chunk-CSTYBKZC.mjs.map +0 -1
- package/dist/chunk-IGK2AP6J.mjs.map +0 -1
- package/dist/chunk-MNDVYPEP.mjs.map +0 -1
- package/dist/chunk-OPPYEA7F.cjs.map +0 -1
- package/dist/chunk-PYJMJF44.cjs.map +0 -1
- package/dist/chunk-RMUWRESK.cjs.map +0 -1
- package/dist/chunk-ZT35X7KL.mjs.map +0 -1
- package/dist/handler-CZIlZ5vu.d.ts +0 -227
- package/dist/handler-kU9Jf36K.d.cts +0 -227
- package/dist/styles.css +0 -367
package/README.md
ADDED
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
# Tappify Extension SDK
|
|
2
|
+
|
|
3
|
+
Build extensions that run inside Tappify. The SDK provides the React host
|
|
4
|
+
bridge, UI components, manifest contracts, Vite integration, server handlers,
|
|
5
|
+
code generation, and test utilities used by Tappify extensions.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pnpm add @tappify/extension-sdk
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
New extensions should use the Tappify CLI, which creates a complete project
|
|
14
|
+
with a manifest, contribution examples, schemas, a server, and tests:
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
npm install --global @tappify/cli
|
|
18
|
+
tappify extension init my-extension
|
|
19
|
+
cd my-extension
|
|
20
|
+
pnpm dev
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
The SDK requires Node.js 20 or newer and React 19.
|
|
24
|
+
|
|
25
|
+
## Package entry points
|
|
26
|
+
|
|
27
|
+
| Import | Purpose |
|
|
28
|
+
| --------------------------------------- | ---------------------------------------------------------------------- |
|
|
29
|
+
| `@tappify/extension-sdk` | React hooks, host types, and query helpers |
|
|
30
|
+
| `@tappify/extension-sdk/ui` | One UI components and layout primitives |
|
|
31
|
+
| `@tappify/extension-sdk/manifest` | Manifest schema, validation, registry constants, and manifest types |
|
|
32
|
+
| `@tappify/extension-sdk/server` | Request handler, token verification, events, and Node/Express adapters |
|
|
33
|
+
| `@tappify/extension-sdk/vite` | Vite and Module Federation configuration |
|
|
34
|
+
| `@tappify/extension-sdk/testing` | Host mocks, fixtures, rendering helpers, and validators |
|
|
35
|
+
| `@tappify/extension-sdk/testing/vitest` | Vitest matcher setup |
|
|
36
|
+
| `@tappify/extension-sdk/testing/jest` | Jest matcher setup |
|
|
37
|
+
| `@tappify/extension-sdk/codegen` | Manifest-driven TypeScript generation |
|
|
38
|
+
| `@tappify/extension-sdk/ui/styles.css` | One UI component styles |
|
|
39
|
+
|
|
40
|
+
## Build a contribution
|
|
41
|
+
|
|
42
|
+
Declare each contribution in `tappify.extension.json`, then export the React
|
|
43
|
+
component from its configured entry file. Host data and capabilities are
|
|
44
|
+
available only when the manifest declares the corresponding scope.
|
|
45
|
+
|
|
46
|
+
```tsx
|
|
47
|
+
import {
|
|
48
|
+
useTapFilters,
|
|
49
|
+
useTapQuery,
|
|
50
|
+
type TapWidgetProps,
|
|
51
|
+
} 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';
|
|
61
|
+
|
|
62
|
+
export default function DownloadsWidget(_props: TapWidgetProps) {
|
|
63
|
+
const filters = useTapFilters();
|
|
64
|
+
const downloads = useTapQuery({
|
|
65
|
+
kind: 'series',
|
|
66
|
+
metric: 'downloads',
|
|
67
|
+
range: filters.range,
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
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>
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
```
|
|
91
|
+
|
|
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
|
+
Add the SDK plugin to the extension's Vite config:
|
|
118
|
+
|
|
119
|
+
```ts
|
|
120
|
+
import { tappifyExtension } from '@tappify/extension-sdk/vite';
|
|
121
|
+
import react from '@vitejs/plugin-react';
|
|
122
|
+
import { defineConfig } from 'vite';
|
|
123
|
+
|
|
124
|
+
export default defineConfig({
|
|
125
|
+
plugins: [react(), tappifyExtension()],
|
|
126
|
+
build: { target: 'esnext' },
|
|
127
|
+
});
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
## Add a server procedure
|
|
131
|
+
|
|
132
|
+
The server entry point exposes a fetch-style handler and adapters for common
|
|
133
|
+
Node runtimes. Procedure inputs, credentials, and responses are typed from the
|
|
134
|
+
extension manifest and its referenced JSON Schemas.
|
|
135
|
+
|
|
136
|
+
```ts
|
|
137
|
+
import {
|
|
138
|
+
createTappifyHandler,
|
|
139
|
+
type TappifyHandlerOptions,
|
|
140
|
+
} from '@tappify/extension-sdk/server';
|
|
141
|
+
|
|
142
|
+
const options: TappifyHandlerOptions = {
|
|
143
|
+
extensionId: 'my-extension',
|
|
144
|
+
health: () => ({ ok: true, version: '1.0.0' }),
|
|
145
|
+
procedures: {
|
|
146
|
+
getSummary: request => ({ projectId: request.context.projectId }),
|
|
147
|
+
},
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
export const handler = createTappifyHandler(options);
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
## Test an extension
|
|
154
|
+
|
|
155
|
+
`renderWithTap` renders a contribution inside the same context contract the
|
|
156
|
+
host provides. It returns a mock for inspecting storage, telemetry, navigation,
|
|
157
|
+
toasts, events, and server calls.
|
|
158
|
+
|
|
159
|
+
```tsx
|
|
160
|
+
import { renderWithTap } from '@tappify/extension-sdk/testing';
|
|
161
|
+
import { screen } from '@testing-library/react';
|
|
162
|
+
import { expect, it } from 'vitest';
|
|
163
|
+
import DownloadsWidget from './DownloadsWidget';
|
|
164
|
+
|
|
165
|
+
it('renders host data', async () => {
|
|
166
|
+
renderWithTap(<DownloadsWidget config={{}} />, {
|
|
167
|
+
scopes: ['ui:render', 'analytics:read'],
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
expect(await screen.findByText('Downloads')).toBeInTheDocument();
|
|
171
|
+
});
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
Register the SDK matchers from the appropriate test setup entry point:
|
|
175
|
+
|
|
176
|
+
```ts
|
|
177
|
+
// Vitest setup file
|
|
178
|
+
import '@tappify/extension-sdk/testing/vitest';
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
## Compatibility and releases
|
|
182
|
+
|
|
183
|
+
The `sdk` field in `tappify.extension.json` is the compatibility contract
|
|
184
|
+
between an extension and the Tappify host. Use a caret range within the major
|
|
185
|
+
version used to build the extension:
|
|
186
|
+
|
|
187
|
+
```json
|
|
188
|
+
{
|
|
189
|
+
"sdk": "^2.0.0"
|
|
190
|
+
}
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
Releases follow semantic versioning. Conventional commits drive
|
|
194
|
+
`semantic-release`: `fix` creates a patch, `feat` creates a minor, and a
|
|
195
|
+
`BREAKING CHANGE` creates a major release. The `main` branch publishes stable
|
|
196
|
+
versions and `develop` publishes the `beta` channel.
|
|
197
|
+
|
|
198
|
+
## Documentation
|
|
199
|
+
|
|
200
|
+
- [Build your first extension](https://docs.tappify.ai/extensions/start/your-first-extension)
|
|
201
|
+
- [Manifest reference](https://docs.tappify.ai/extensions/reference/manifest)
|
|
202
|
+
- [Frontend API](https://docs.tappify.ai/extensions/reference/frontend-api)
|
|
203
|
+
- [Server API](https://docs.tappify.ai/extensions/reference/server-api)
|
|
204
|
+
- [Testing API](https://docs.tappify.ai/extensions/reference/testing-api)
|
|
205
|
+
|
|
206
|
+
## License
|
|
207
|
+
|
|
208
|
+
MIT
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import {
|
|
2
|
+
dataQueryKey,
|
|
3
|
+
installPrefix,
|
|
4
|
+
procedurePrefix,
|
|
5
|
+
procedureQueryKey,
|
|
6
|
+
storageQueryKey,
|
|
7
|
+
useTapAuth,
|
|
8
|
+
useTapContext,
|
|
9
|
+
useTapFilters,
|
|
10
|
+
useTapParams,
|
|
11
|
+
useTapProject,
|
|
12
|
+
useTapQuery,
|
|
13
|
+
useTapServer,
|
|
14
|
+
useTapSize,
|
|
15
|
+
useTapState,
|
|
16
|
+
useTapStorage,
|
|
17
|
+
useTapTheme
|
|
18
|
+
} from "./chunk-SGEYDV3K.mjs";
|
|
19
|
+
import {
|
|
20
|
+
useTap
|
|
21
|
+
} from "./chunk-QA5AL3VZ.mjs";
|
|
22
|
+
import {
|
|
23
|
+
TAP_ERROR_CODES,
|
|
24
|
+
TapError,
|
|
25
|
+
TapServerError
|
|
26
|
+
} from "./chunk-QXD7WVKK.mjs";
|
|
27
|
+
import {
|
|
28
|
+
__export
|
|
29
|
+
} from "./chunk-J5LGTIGS.mjs";
|
|
30
|
+
|
|
31
|
+
// src/index.ts
|
|
32
|
+
var src_exports = {};
|
|
33
|
+
__export(src_exports, {
|
|
34
|
+
TAP_ERROR_CODES: () => TAP_ERROR_CODES,
|
|
35
|
+
TapError: () => TapError,
|
|
36
|
+
TapServerError: () => TapServerError,
|
|
37
|
+
dataQueryKey: () => dataQueryKey,
|
|
38
|
+
installPrefix: () => installPrefix,
|
|
39
|
+
procedurePrefix: () => procedurePrefix,
|
|
40
|
+
procedureQueryKey: () => procedureQueryKey,
|
|
41
|
+
storageQueryKey: () => storageQueryKey,
|
|
42
|
+
useTap: () => useTap,
|
|
43
|
+
useTapAuth: () => useTapAuth,
|
|
44
|
+
useTapContext: () => useTapContext,
|
|
45
|
+
useTapFilters: () => useTapFilters,
|
|
46
|
+
useTapParams: () => useTapParams,
|
|
47
|
+
useTapProject: () => useTapProject,
|
|
48
|
+
useTapQuery: () => useTapQuery,
|
|
49
|
+
useTapServer: () => useTapServer,
|
|
50
|
+
useTapSize: () => useTapSize,
|
|
51
|
+
useTapState: () => useTapState,
|
|
52
|
+
useTapStorage: () => useTapStorage,
|
|
53
|
+
useTapTheme: () => useTapTheme
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
export {
|
|
57
|
+
src_exports
|
|
58
|
+
};
|
|
59
|
+
//# sourceMappingURL=chunk-2CY5HVY7.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["/**\n * @module index\n * @mergeModuleWith <project>\n */\nexport { useTap } from './client/context';\nexport { TAP_ERROR_CODES, TapError, TapServerError } from './client/errors';\nexport type { TapErrorCode } from './client/errors';\nexport {\n useTapAuth,\n useTapContext,\n useTapFilters,\n useTapParams,\n useTapProject,\n useTapSize,\n useTapState,\n useTapTheme,\n} from './client/hooks';\nexport {\n dataQueryKey,\n installPrefix,\n procedurePrefix,\n procedureQueryKey,\n storageQueryKey,\n useTapQuery,\n useTapServer,\n useTapStorage,\n} from './client/query';\nexport type {\n TapQueryOptions,\n TapQueryResult,\n TapStorageResult,\n} from './client/query';\nexport type * from './client/types';\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;","names":[]}
|
|
@@ -0,0 +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"]}
|
|
@@ -35,6 +35,13 @@ var SCOPES = [
|
|
|
35
35
|
requiresSecurityReview: false,
|
|
36
36
|
requiresJustification: false
|
|
37
37
|
},
|
|
38
|
+
{
|
|
39
|
+
key: "review.threads:read",
|
|
40
|
+
label: "Read Review threads",
|
|
41
|
+
description: "Review threads, comments, screenshot descriptors, build context and safe device context",
|
|
42
|
+
requiresSecurityReview: false,
|
|
43
|
+
requiresJustification: false
|
|
44
|
+
},
|
|
38
45
|
{
|
|
39
46
|
key: "crashes:read",
|
|
40
47
|
label: "Read crash data",
|
|
@@ -108,7 +115,7 @@ var SCOPES = [
|
|
|
108
115
|
{
|
|
109
116
|
key: "work:sync",
|
|
110
117
|
label: "Sync work items",
|
|
111
|
-
description: "
|
|
118
|
+
description: "Sync comments and status on created work items",
|
|
112
119
|
requiresSecurityReview: false,
|
|
113
120
|
requiresJustification: true
|
|
114
121
|
},
|
|
@@ -299,4 +306,4 @@ var slotsForPage = (page) => _nullishCoalesce(_optionalChain([findPage, 'call',
|
|
|
299
306
|
|
|
300
307
|
|
|
301
308
|
exports.SCOPES = SCOPES; exports.SCOPE_KEYS = SCOPE_KEYS; exports.SLOTS = SLOTS; exports.CHARTS = CHARTS; exports.PAGES = PAGES; exports.PAGE_KEYS = PAGE_KEYS; exports.TABLES = TABLES; exports.TABLE_KEYS = TABLE_KEYS; exports.KINDS = KINDS; exports.CATEGORIES = CATEGORIES; exports.EVENTS = EVENTS; exports.SERVED_SDK_MAJORS = SERVED_SDK_MAJORS; exports.QUERY_SCOPES = QUERY_SCOPES; exports.CARD_KEYS = CARD_KEYS; exports.findScope = findScope; exports.findPage = findPage; exports.findTable = findTable; exports.slotsForPage = slotsForPage;
|
|
302
|
-
//# sourceMappingURL=chunk-
|
|
309
|
+
//# sourceMappingURL=chunk-7YKH4LZN.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["/home/runner/work/extension-sdk/extension-sdk/dist/chunk-7YKH4LZN.cjs","../src/manifest/constants.ts"],"names":[],"mappings":"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","file":"/home/runner/work/extension-sdk/extension-sdk/dist/chunk-7YKH4LZN.cjs","sourcesContent":[null,"import type { ToolDeclaration } from './types';\n\nexport const SCOPES = [\n {\n key: 'ui:render',\n label: 'Render UI',\n description:\n 'Render the declared pages, tabs, widgets, row actions and settings panel, and open chat with a visible prompt',\n requiresSecurityReview: false,\n requiresJustification: false,\n },\n {\n key: 'projects:read',\n label: 'Read the project',\n description: 'Project name, platforms and store identity',\n requiresSecurityReview: false,\n requiresJustification: false,\n },\n {\n key: 'analytics:read',\n label: 'Read analytics',\n description: 'Downloads, impressions, page views and conversion series',\n requiresSecurityReview: false,\n requiresJustification: false,\n },\n {\n key: 'store.metadata:read',\n label: 'Read store metadata',\n description: 'Listing metadata, keyword set and keyword positions',\n requiresSecurityReview: false,\n requiresJustification: false,\n },\n {\n key: 'reviews:read',\n label: 'Read store reviews',\n description: 'Review text, ratings and replies',\n requiresSecurityReview: false,\n requiresJustification: false,\n },\n {\n key: 'review.threads:read',\n label: 'Read Review threads',\n description:\n 'Review threads, comments, screenshot descriptors, build context and safe device context',\n requiresSecurityReview: false,\n requiresJustification: false,\n },\n {\n key: 'crashes:read',\n label: 'Read crash data',\n description: 'Crash-free rate and issue counts',\n requiresSecurityReview: false,\n requiresJustification: false,\n },\n {\n key: 'revenue:read',\n label: 'Read revenue',\n description: 'MRR, revenue series and refund counts',\n requiresSecurityReview: false,\n requiresJustification: true,\n },\n {\n key: 'metrics:write',\n label: 'Write connector metrics',\n description:\n 'Write the metrics this extension declares into the metrics store',\n requiresSecurityReview: false,\n requiresJustification: false,\n },\n {\n key: 'alerts:write',\n label: 'Raise alerts and banners',\n description:\n 'Turn declared webhooks into alerts and banners, at most three a day per project',\n requiresSecurityReview: false,\n requiresJustification: true,\n },\n {\n key: 'insights:write',\n label: 'Post insights',\n description: 'Post insights to Home and to chat',\n requiresSecurityReview: false,\n requiresJustification: true,\n },\n {\n key: 'ai:tools',\n label: 'Add assistant tools',\n description: 'Offer tools and mentions to the assistant',\n requiresSecurityReview: false,\n requiresJustification: false,\n },\n {\n key: 'ai:actions',\n label: 'Run assistant actions',\n description: 'Run declared actions, each behind the approval card',\n requiresSecurityReview: true,\n requiresJustification: true,\n },\n {\n key: 'store.metadata:write',\n label: 'Propose store metadata changes',\n description: 'Propose listing changes through a Tappify approval',\n requiresSecurityReview: true,\n requiresJustification: true,\n },\n {\n key: 'autopilot:trigger',\n label: 'Trigger automated store actions',\n description: 'Start automated store actions on the owner’s behalf',\n requiresSecurityReview: true,\n requiresJustification: true,\n },\n {\n key: 'work:create',\n label: 'Create work items',\n description: 'Create issues, pages or messages in the vendor’s system',\n requiresSecurityReview: false,\n requiresJustification: true,\n },\n {\n key: 'work:sync',\n label: 'Sync work items',\n description: 'Sync comments and status on created work items',\n requiresSecurityReview: false,\n requiresJustification: true,\n },\n {\n key: 'storage:write',\n label: 'Use hosted storage',\n description:\n 'Read and write this extension’s own hosted storage collections',\n requiresSecurityReview: false,\n requiresJustification: false,\n },\n {\n key: 'messaging:send',\n label: 'Send messages to end users',\n description:\n 'Send messages or push notifications to the owner’s end users through owner-supplied credentials',\n requiresSecurityReview: true,\n requiresJustification: true,\n },\n {\n key: 'ai:skills',\n label: 'Shape the assistant',\n description:\n 'Add skills, prompt templates and context providers that shape the assistant’s behaviour',\n requiresSecurityReview: false,\n requiresJustification: true,\n },\n] as const;\n\nexport type ScopeDefinition = (typeof SCOPES)[number];\n/** Every scope an extension can ask for under `scopes` in its manifest. */\nexport type ScopeKey = ScopeDefinition['key'];\n\nexport const SCOPE_KEYS: readonly ScopeKey[] = SCOPES.map(scope => scope.key);\n\nexport const SLOTS = [\n 'kpi-row',\n 'insights',\n 'sidebar',\n 'below-chart',\n 'below-list',\n] as const;\n/** The anchors on a host page a widget can be declared at. */\nexport type SlotId = (typeof SLOTS)[number];\n\nexport const CHARTS = [\n 'installs-activity',\n 'conversion',\n 'release-cadence',\n] as const;\nexport type HostChart = (typeof CHARTS)[number];\n\nexport const PAGES = [\n {\n key: 'overview',\n label: 'Home',\n route: '/projects/:id/overview',\n slots: ['kpi-row', 'insights', 'sidebar'],\n charts: ['installs-activity'],\n tabs: true,\n banners: true,\n },\n {\n key: 'analytics',\n label: 'Analytics',\n route: '/projects/:id/analytics',\n slots: ['kpi-row', 'below-chart', 'sidebar'],\n charts: ['installs-activity', 'conversion'],\n tabs: true,\n banners: true,\n },\n {\n key: 'deployments',\n label: 'Deployments',\n route: '/projects/:id/deployments',\n slots: ['sidebar', 'below-list'],\n charts: ['release-cadence'],\n tabs: true,\n banners: false,\n },\n {\n key: 'ai_chat',\n label: 'Assistant',\n route: 'chat',\n slots: [],\n charts: [],\n tabs: false,\n banners: false,\n },\n] as const;\n\nexport type PageDefinition = (typeof PAGES)[number];\n/** The host pages a contribution can attach a tab, widget or banner to. */\nexport type HostPage = PageDefinition['key'];\n\nexport const PAGE_KEYS: readonly HostPage[] = PAGES.map(page => page.key);\n\nexport const TABLES = [\n { key: 'analytics.keywords', label: 'Keywords' },\n { key: 'analytics.reviews', label: 'Reviews' },\n { key: 'deployments.releases', label: 'Releases' },\n { key: 'deployments.builds', label: 'Builds' },\n] as const;\n\nexport type TableDefinition = (typeof TABLES)[number];\nexport type HostTable = TableDefinition['key'];\n\nexport const TABLE_KEYS: readonly HostTable[] = TABLES.map(table => table.key);\n\nexport const KINDS = [\n 'page',\n 'tab',\n 'widget',\n 'series',\n 'banner',\n 'rowAction',\n 'marker',\n 'settings',\n 'storage',\n 'connector',\n 'webhook',\n 'tool',\n 'mention',\n 'action',\n 'knowledge',\n 'skill',\n 'prompt',\n 'context',\n 'work',\n] as const;\nexport type ContributionKind = (typeof KINDS)[number];\n\nexport const CATEGORIES = [\n 'market_data',\n 'subscriptions',\n 'product_analytics',\n 'crashes',\n 'attribution',\n 'experiments',\n 'messaging',\n 'session_replay',\n 'localization',\n 'design',\n 'ci_cd',\n 'trackers',\n 'support',\n 'chat',\n 'ad_monetization',\n 'warehouse',\n 'compliance',\n 'additional_stores',\n] as const;\nexport type ExtensionCategory = (typeof CATEGORIES)[number];\n\nexport const EVENTS = [\n 'release.shipped',\n 'metadata.changed',\n 'keyword.set_changed',\n 'screenshots.updated',\n 'price.changed',\n 'featuring.started',\n 'score.changed',\n 'incident.opened',\n 'incident.resolved',\n 'playbook.step_approved',\n 'review.thread_opened',\n 'review.thread_resolved',\n 'approval.requested',\n 'digest.sent',\n 'install.created',\n 'install.paused',\n 'install.resumed',\n 'install.revoked',\n 'install.token_rotated',\n 'scopes.changed',\n 'settings.changed',\n] as const;\n/** Every event Tappify emits, which a mount or a server can subscribe to. */\nexport type HostEventName = (typeof EVENTS)[number];\n\nexport const SERVED_SDK_MAJORS: readonly number[] = [2];\n\n/** The `kind` of every read `tap.data.query` accepts, one per scope it needs. */\nexport type DataQueryKind =\n | 'project'\n | 'series'\n | 'keywords'\n | 'listing'\n | 'reviews'\n | 'crashes'\n | 'revenue';\n\nexport const QUERY_SCOPES: Record<DataQueryKind, ScopeKey> = {\n project: 'projects:read',\n series: 'analytics:read',\n keywords: 'store.metadata:read',\n listing: 'store.metadata:read',\n reviews: 'reviews:read',\n crashes: 'crashes:read',\n revenue: 'revenue:read',\n};\n\n/**\n * The keys a tool's answer carries for each `returns` kind it may declare.\n * `validateToolResponse`, `tappify extension doctor` and the host all read\n * this one table, so a wrong body fails the same way wherever it is checked.\n */\nexport const CARD_KEYS: Record<ToolDeclaration['returns'], string[]> = {\n value: ['label', 'value'],\n series: ['label', 'points'],\n table: ['columns', 'rows'],\n list: ['items'],\n comparison: ['label', 'left', 'right'],\n};\n\nexport const findScope = (key: string): ScopeDefinition | undefined =>\n SCOPES.find(scope => scope.key === key);\n\nexport const findPage = (key: string): PageDefinition | undefined =>\n PAGES.find(page => page.key === key);\n\nexport const findTable = (key: string): TableDefinition | undefined =>\n TABLES.find(table => table.key === key);\n\nexport const slotsForPage = (page: string): readonly SlotId[] =>\n findPage(page)?.slots ?? [];\n"]}
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
"use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _nullishCoalesce(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return rhsFn(); } } function _optionalChain(ops) { let lastAccessLHS = undefined; let value = ops[0]; let i = 1; while (i < ops.length) { const op = ops[i]; const fn = ops[i + 1]; i += 2; if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { return undefined; } if (op === 'access' || op === 'optionalAccess') { lastAccessLHS = value; value = fn(value); } else if (op === 'call' || op === 'optionalCall') { value = fn((...args) => value.call(lastAccessLHS, ...args)); lastAccessLHS = undefined; } } return value; }
|
|
2
2
|
|
|
3
|
-
var
|
|
3
|
+
var _chunk7YKH4LZNcjs = require('./chunk-7YKH4LZN.cjs');
|
|
4
4
|
|
|
5
5
|
|
|
6
6
|
|
|
7
|
-
var
|
|
7
|
+
var _chunk6HETNZJYcjs = require('./chunk-6HETNZJY.cjs');
|
|
8
8
|
|
|
9
9
|
// src/server/codes.ts
|
|
10
10
|
var SERVER_ERROR_CODES = {
|
|
@@ -46,7 +46,7 @@ function toClaims(payload, audience) {
|
|
|
46
46
|
const aud = payload.aud;
|
|
47
47
|
const matches = aud === audience || Array.isArray(aud) && aud.includes(audience);
|
|
48
48
|
if (!matches) {
|
|
49
|
-
throw new (0,
|
|
49
|
+
throw new (0, _chunk6HETNZJYcjs.TapError)(
|
|
50
50
|
"TAP_TOKEN_AUDIENCE",
|
|
51
51
|
`This install token was issued for a different extension. Create the handler with the same extensionId as the "id" in tappify.extension.json, so its audience is "${audience}".`
|
|
52
52
|
);
|
|
@@ -55,7 +55,7 @@ function toClaims(payload, audience) {
|
|
|
55
55
|
const extensionId = readString(payload, "extensionId");
|
|
56
56
|
const organizationId = readString(payload, "organizationId");
|
|
57
57
|
if (installId === void 0 || extensionId === void 0 || organizationId === void 0) {
|
|
58
|
-
throw new (0,
|
|
58
|
+
throw new (0, _chunk6HETNZJYcjs.TapError)(
|
|
59
59
|
"TAP_TOKEN_INVALID",
|
|
60
60
|
`The install token is missing the installId, extensionId or organizationId claim. Verify against ${DEFAULT_JWKS_URL} and do not reuse tokens across environments.`
|
|
61
61
|
);
|
|
@@ -86,7 +86,7 @@ async function verifyTappifyToken(token, options) {
|
|
|
86
86
|
});
|
|
87
87
|
payload = verified.payload;
|
|
88
88
|
} catch (e) {
|
|
89
|
-
throw new (0,
|
|
89
|
+
throw new (0, _chunk6HETNZJYcjs.TapError)(
|
|
90
90
|
"TAP_TOKEN_INVALID",
|
|
91
91
|
"The install token did not verify. Check that your server reads the Authorization header unchanged, that its clock is correct, and that jwksUrl points at the Tappify environment that called you."
|
|
92
92
|
);
|
|
@@ -124,10 +124,15 @@ function readText(value, key) {
|
|
|
124
124
|
// src/server/handler.ts
|
|
125
125
|
var MAX_BODY_BYTES = 1048576;
|
|
126
126
|
var WORK_OPERATIONS = [
|
|
127
|
+
"configure",
|
|
127
128
|
"list_containers",
|
|
129
|
+
"describe_container",
|
|
128
130
|
"create",
|
|
129
131
|
"attach",
|
|
130
132
|
"comment",
|
|
133
|
+
"set_status",
|
|
134
|
+
"hydrate_event",
|
|
135
|
+
"disconnect",
|
|
131
136
|
"status",
|
|
132
137
|
"resolve"
|
|
133
138
|
];
|
|
@@ -183,7 +188,7 @@ function toCredentials(payload) {
|
|
|
183
188
|
return credentials;
|
|
184
189
|
}
|
|
185
190
|
function tooLarge() {
|
|
186
|
-
return new (0,
|
|
191
|
+
return new (0, _chunk6HETNZJYcjs.TapServerError)(
|
|
187
192
|
SERVER_ERROR_CODES.BODY_TOO_LARGE,
|
|
188
193
|
"A Tappify request body is at most 1 MB. Return a cursor or an id the host can follow instead of inlining the payload.",
|
|
189
194
|
413
|
|
@@ -200,7 +205,7 @@ async function readBody(request) {
|
|
|
200
205
|
try {
|
|
201
206
|
return JSON.parse(text);
|
|
202
207
|
} catch (e2) {
|
|
203
|
-
throw new (0,
|
|
208
|
+
throw new (0, _chunk6HETNZJYcjs.TapServerError)(
|
|
204
209
|
SERVER_ERROR_CODES.BODY_INVALID,
|
|
205
210
|
"The request body was not JSON. Do not parse or re-encode the body before handing the Request to createTappifyHandler().",
|
|
206
211
|
400
|
|
@@ -238,24 +243,31 @@ function resolveRoute(options, method, segments, url, payload) {
|
|
|
238
243
|
}
|
|
239
244
|
if (head === "work" && method === "POST" && name !== void 0) {
|
|
240
245
|
const operation = WORK_OPERATIONS.find((candidate) => candidate === name);
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
246
|
+
const handler = operation === void 0 ? void 0 : _optionalChain([options, 'access', _2 => _2.work, 'optionalAccess', _3 => _3[operation]]);
|
|
247
|
+
if (operation === "set_status" && handler === void 0) {
|
|
248
|
+
const input = readRecord(payload, "input");
|
|
249
|
+
if (readText(input, "tappifyStatus") === "resolved") {
|
|
250
|
+
return route(_optionalChain([options, 'access', _4 => _4.work, 'optionalAccess', _5 => _5.resolve]), {
|
|
251
|
+
...input,
|
|
252
|
+
id: readUnknown(input, "externalItemId")
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
return route(handler, bodyInput);
|
|
245
257
|
}
|
|
246
258
|
if (head === "events" && method === "POST") {
|
|
247
259
|
const envelope = readRecord(payload, "event");
|
|
248
260
|
const eventName = readText(envelope, "name");
|
|
249
261
|
if (eventName === null) {
|
|
250
|
-
throw new (0,
|
|
262
|
+
throw new (0, _chunk6HETNZJYcjs.TapServerError)(
|
|
251
263
|
SERVER_ERROR_CODES.BODY_INVALID,
|
|
252
264
|
'The events body has no event.name. Post the envelope Tappify sends unchanged, with the event name under "event".',
|
|
253
265
|
400
|
|
254
266
|
);
|
|
255
267
|
}
|
|
256
|
-
const declared =
|
|
268
|
+
const declared = _chunk7YKH4LZNcjs.EVENTS.find((candidate) => candidate === eventName);
|
|
257
269
|
return route(
|
|
258
|
-
declared === void 0 ? void 0 : _optionalChain([options, 'access',
|
|
270
|
+
declared === void 0 ? void 0 : _optionalChain([options, 'access', _6 => _6.events, 'optionalAccess', _7 => _7[declared]]),
|
|
259
271
|
void 0,
|
|
260
272
|
_nullishCoalesce(readUnknown(envelope, "payload"), () => ( {})),
|
|
261
273
|
true
|
|
@@ -335,10 +347,10 @@ function createTappifyHandler(options) {
|
|
|
335
347
|
const result = await matched.handler(typed);
|
|
336
348
|
return matched.noContent ? new Response(null, { status: 204 }) : json(result);
|
|
337
349
|
} catch (error) {
|
|
338
|
-
if (
|
|
350
|
+
if (_chunk6HETNZJYcjs.TapServerError.is(error)) {
|
|
339
351
|
return wireError(error.code, error.message, _nullishCoalesce(error.status, () => ( 400)));
|
|
340
352
|
}
|
|
341
|
-
if (
|
|
353
|
+
if (_chunk6HETNZJYcjs.TapError.is(error)) {
|
|
342
354
|
const tokenFailure = TOKEN_FAILURES.find(
|
|
343
355
|
(candidate) => candidate === error.code
|
|
344
356
|
);
|
|
@@ -361,5 +373,6 @@ function createTappifyHandler(options) {
|
|
|
361
373
|
|
|
362
374
|
|
|
363
375
|
|
|
364
|
-
|
|
365
|
-
|
|
376
|
+
|
|
377
|
+
exports.SERVER_ERROR_CODES = SERVER_ERROR_CODES; exports.DEFAULT_JWKS_URL = DEFAULT_JWKS_URL; exports.verifyTappifyToken = verifyTappifyToken; exports.INTERNAL_MESSAGE = INTERNAL_MESSAGE; exports.wireError = wireError; exports.readUnknown = readUnknown; exports.readRecord = readRecord; exports.readText = readText; exports.createTappifyHandler = createTappifyHandler;
|
|
378
|
+
//# sourceMappingURL=chunk-A74HCO6T.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["/home/runner/work/extension-sdk/extension-sdk/dist/chunk-A74HCO6T.cjs","../src/server/codes.ts","../src/server/token.ts","../src/server/wire.ts","../src/server/handler.ts"],"names":[],"mappings":"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","file":"/home/runner/work/extension-sdk/extension-sdk/dist/chunk-A74HCO6T.cjs","sourcesContent":[null,"import type { TapErrorCode } from '../client/errors';\n\n/**\n * The codes `createTappifyHandler` answers with itself, keyed by a short name.\n *\n * @remarks\n * Every one of these comes from the SDK, not from your own handlers: a route or\n * handler that is not there, a token that is missing or does not verify, a missing\n * event id, a body that is not JSON or is over 1 MB, a webhook Tappify rejected,\n * and the catch-all for an error a handler let escape. Throw a `TapServerError`\n * with a code of your own for anything your server decides.\n */\nexport const SERVER_ERROR_CODES = {\n ROUTE_UNKNOWN: 'TAP_ROUTE_UNKNOWN',\n HANDLER_MISSING: 'TAP_HANDLER_MISSING',\n INTERNAL_ERROR: 'TAP_INTERNAL_ERROR',\n TOKEN_MISSING: 'TAP_TOKEN_MISSING',\n TOKEN_INVALID: 'TAP_TOKEN_INVALID',\n TOKEN_AUDIENCE: 'TAP_TOKEN_AUDIENCE',\n EVENT_ID_MISSING: 'TAP_EVENT_ID_MISSING',\n BODY_INVALID: 'TAP_BODY_INVALID',\n BODY_TOO_LARGE: 'TAP_BODY_TOO_LARGE',\n WEBHOOK_REJECTED: 'TAP_WEBHOOK_REJECTED',\n} as const satisfies Record<string, TapErrorCode>;\n\n/** Every code the handler answers with itself, as a union of its own values. */\nexport type TapServerErrorCode =\n (typeof SERVER_ERROR_CODES)[keyof typeof SERVER_ERROR_CODES];\n","import {\n createLocalJWKSet,\n createRemoteJWKSet,\n jwtVerify,\n type JWTPayload,\n type JWTVerifyGetKey,\n} from 'jose';\nimport { TapError } from '../client/errors';\nimport type { TappifyClaims, VerifyTokenOptions } from './types';\n\n/**\n * The key set `verifyTappifyToken` fetches when no `jwksUrl` or `jwks` is given.\n *\n * @remarks\n * This is production. Point `jwksUrl` at the environment that is calling you when\n * you run against another one, and pass `jwks` in tests.\n */\nexport const DEFAULT_JWKS_URL =\n 'https://api.tappify.ai/api/v1/extensions/.well-known/jwks.json';\n\nconst CLOCK_TOLERANCE_SECONDS = 60;\n\nconst remoteSets = new Map<string, JWTVerifyGetKey>();\n\nfunction keySet(options: VerifyTokenOptions): JWTVerifyGetKey {\n if (options.jwks) return createLocalJWKSet(options.jwks);\n\n const url = options.jwksUrl ?? DEFAULT_JWKS_URL;\n const cached = remoteSets.get(url);\n if (cached) return cached;\n\n const created = createRemoteJWKSet(new URL(url));\n remoteSets.set(url, created);\n return created;\n}\n\nfunction readString(payload: JWTPayload, key: string): string | undefined {\n const value = payload[key];\n return typeof value === 'string' ? value : undefined;\n}\n\nfunction toClaims(payload: JWTPayload, audience: string): TappifyClaims {\n const aud = payload.aud;\n const matches =\n aud === audience || (Array.isArray(aud) && aud.includes(audience));\n\n if (!matches) {\n throw new TapError(\n 'TAP_TOKEN_AUDIENCE',\n `This install token was issued for a different extension. Create the handler with the same extensionId as the \"id\" in tappify.extension.json, so its audience is \"${audience}\".`,\n );\n }\n\n const installId = readString(payload, 'installId');\n const extensionId = readString(payload, 'extensionId');\n const organizationId = readString(payload, 'organizationId');\n\n if (\n installId === undefined ||\n extensionId === undefined ||\n organizationId === undefined\n ) {\n throw new TapError(\n 'TAP_TOKEN_INVALID',\n `The install token is missing the installId, extensionId or organizationId claim. Verify against ${DEFAULT_JWKS_URL} and do not reuse tokens across environments.`,\n );\n }\n\n const scopes = payload.scopes;\n const userId = readString(payload, 'userId');\n\n const claims: TappifyClaims = {\n iss: readString(payload, 'iss') ?? '',\n aud: audience,\n exp: typeof payload.exp === 'number' ? payload.exp : 0,\n iat: typeof payload.iat === 'number' ? payload.iat : 0,\n installId,\n extensionId,\n projectId: readString(payload, 'projectId') ?? null,\n organizationId,\n scopes: Array.isArray(scopes)\n ? scopes.filter((scope): scope is string => typeof scope === 'string')\n : [],\n };\n\n if (userId !== undefined) claims.userId = userId;\n return claims;\n}\n\n/**\n * Verifies an install token against Tappify's signing keys and returns its\n * claims.\n *\n * @remarks\n * Checks the RS256 signature, the expiry with 60 seconds of clock tolerance, and\n * that the audience is `ext:<extensionId>`. Rejects with a `TapError` carrying\n * `TAP_TOKEN_INVALID` when the signature, the expiry or a required claim is wrong,\n * and `TAP_TOKEN_AUDIENCE` when the token was issued for another extension. A\n * remote key set is fetched once per url and cached for the process, so calling\n * this per request costs nothing after the first. `createTappifyHandler` already\n * verifies every route but health, so call this only outside the handler, or\n * where you accept a Tappify token on a route of your own.\n *\n * @example\n * ```ts\n * import { verifyTappifyToken } from '@tappify/extension-sdk/server';\n *\n * async function whoCalled(request: Request): Promise<string> {\n * const token = (request.headers.get('authorization') ?? '').replace('Bearer ', '');\n * const claims = await verifyTappifyToken(token, { extensionId: 'starter' });\n * return claims.installId;\n * }\n * ```\n */\nexport async function verifyTappifyToken(\n token: string,\n options: VerifyTokenOptions,\n): Promise<TappifyClaims> {\n const audience = `ext:${options.extensionId}`;\n\n let payload: JWTPayload;\n try {\n const verified = await jwtVerify(token, keySet(options), {\n algorithms: ['RS256'],\n clockTolerance: CLOCK_TOLERANCE_SECONDS,\n });\n payload = verified.payload;\n } catch {\n throw new TapError(\n 'TAP_TOKEN_INVALID',\n 'The install token did not verify. Check that your server reads the Authorization header unchanged, that its clock is correct, and that jwksUrl points at the Tappify environment that called you.',\n );\n }\n\n return toClaims(payload, audience);\n}\n","import type { TapServerErrorCode } from './codes';\n\nexport const INTERNAL_MESSAGE =\n 'The extension server failed to handle this call. Check your own logs; Tappify records the failure against your extension.';\n\nexport function json(data: unknown, status = 200): Response {\n return new Response(JSON.stringify(data), {\n status,\n headers: { 'content-type': 'application/json' },\n });\n}\n\nexport function wireError(\n code: string,\n message: string,\n status: number,\n): Response {\n return json({ error: { code, message } }, status);\n}\n\nexport function failure(\n code: TapServerErrorCode,\n message: string,\n status: number,\n): Response {\n return wireError(code, message, status);\n}\n\nexport function readUnknown(value: unknown, key: string): unknown {\n if (typeof value !== 'object' || value === null) return undefined;\n return (value as Record<string, unknown>)[key];\n}\n\nexport function readRecord(\n value: unknown,\n key: string,\n): Record<string, unknown> {\n const nested = readUnknown(value, key);\n return typeof nested === 'object' && nested !== null\n ? (nested as Record<string, unknown>)\n : {};\n}\n\nexport function readText(value: unknown, key: string): string | null {\n const nested = readUnknown(value, key);\n return typeof nested === 'string' ? nested : null;\n}\n","import { TapError, TapServerError } from '../client/errors';\nimport type { TapCredentialValues, TapDateRange } from '../client/types';\nimport { EVENTS } from '../manifest/constants';\nimport { SERVER_ERROR_CODES, type TapServerErrorCode } from './codes';\nimport { verifyTappifyToken } from './token';\nimport type {\n ContextBlock,\n LegacyWorkHandlers,\n MentionItems,\n MetricsInput,\n MetricsResponse,\n TappifyCallContext,\n TappifyClaims,\n TappifyDocuments,\n TappifyHandlerOptions,\n TappifyHealth,\n TappifyInstall,\n TappifyRequest,\n WorkOperation,\n} from './types';\nimport {\n INTERNAL_MESSAGE,\n failure,\n json,\n readRecord,\n readText,\n readUnknown,\n wireError,\n} from './wire';\n\n/**\n * What `createTappifyHandler` returns: a `fetch` handler, which is what Cloudflare\n * Workers, Vercel and the two adapters all take.\n */\nexport type TappifyFetchHandler = (request: Request) => Promise<Response>;\n\nconst MAX_BODY_BYTES = 1_048_576;\n\nconst WORK_OPERATIONS: readonly (WorkOperation | keyof LegacyWorkHandlers)[] = [\n 'configure',\n 'list_containers',\n 'describe_container',\n 'create',\n 'attach',\n 'comment',\n 'set_status',\n 'hydrate_event',\n 'disconnect',\n 'status',\n 'resolve',\n];\n\nconst TOKEN_FAILURES: readonly TapServerErrorCode[] = [\n SERVER_ERROR_CODES.TOKEN_INVALID,\n SERVER_ERROR_CODES.TOKEN_AUDIENCE,\n];\n\n/**\n * The token is signed by Tappify and the body is not, so the claims win wherever\n * they carry a value and the body only fills what the token leaves out.\n */\nfunction toInstall(payload: unknown, claims: TappifyClaims): TappifyInstall {\n const raw = readRecord(payload, 'install');\n return {\n id: claims.installId,\n extensionId: claims.extensionId,\n projectId: claims.projectId ?? readText(raw, 'projectId'),\n organizationId: claims.organizationId,\n };\n}\n\nfunction toPlatform(value: string | null): TappifyCallContext['platform'] {\n return value === 'ios' || value === 'android' ? value : 'all';\n}\n\nfunction toPreset(value: string | null): TapDateRange['preset'] {\n return value === '7d' || value === '30d' || value === '90d'\n ? value\n : undefined;\n}\n\nfunction toContext(payload: unknown): TappifyCallContext {\n const raw = readRecord(payload, 'context');\n const filters = readRecord(raw, 'filters');\n const rawRange = readRecord(filters, 'range');\n const from = readText(raw, 'from') ?? readText(rawRange, 'from') ?? '';\n const to = readText(raw, 'to') ?? readText(rawRange, 'to') ?? '';\n\n const range: TapDateRange = {\n from: readText(rawRange, 'from') ?? from,\n to: readText(rawRange, 'to') ?? to,\n };\n\n const preset = toPreset(readText(rawRange, 'preset'));\n if (preset !== undefined) range.preset = preset;\n\n return {\n projectId: readText(raw, 'projectId'),\n platform: toPlatform(readText(raw, 'platform')),\n from,\n to,\n filters: {\n range,\n platform: toPlatform(readText(filters, 'platform')),\n country: readText(filters, 'country') ?? 'all',\n },\n };\n}\n\nfunction toCredentials(payload: unknown): TapCredentialValues {\n const raw = readRecord(payload, 'credentials');\n const credentials: Record<string, string> = {};\n for (const [name, value] of Object.entries(raw)) {\n if (typeof value === 'string') credentials[name] = value;\n }\n return credentials;\n}\n\nfunction tooLarge(): TapServerError {\n return new TapServerError(\n SERVER_ERROR_CODES.BODY_TOO_LARGE,\n 'A Tappify request body is at most 1 MB. Return a cursor or an id the host can follow instead of inlining the payload.',\n 413,\n );\n}\n\nasync function readBody(request: Request): Promise<unknown> {\n if (request.method === 'GET' || request.method === 'HEAD') return {};\n\n const length = request.headers.get('content-length');\n if (length !== null && Number(length) > MAX_BODY_BYTES) throw tooLarge();\n\n const bytes = await request.arrayBuffer();\n if (bytes.byteLength > MAX_BODY_BYTES) throw tooLarge();\n if (bytes.byteLength === 0) return {};\n\n const text = new TextDecoder().decode(bytes);\n try {\n return JSON.parse(text);\n } catch {\n throw new TapServerError(\n SERVER_ERROR_CODES.BODY_INVALID,\n 'The request body was not JSON. Do not parse or re-encode the body before handing the Request to createTappifyHandler().',\n 400,\n );\n }\n}\n\n/**\n * The route table holds handlers whose declared input and event types the\n * manifest augmentations narrow. Method syntax keeps the parameter bivariant,\n * so a narrowed handler still fits a table the wire types as `unknown`.\n */\ntype AnyRouteHandler = {\n route(request: TappifyRequest<unknown, unknown>): unknown;\n}['route'];\n\ninterface Route {\n handler: AnyRouteHandler | undefined;\n input: unknown;\n event: unknown;\n noContent: boolean;\n}\n\nfunction route(\n handler: AnyRouteHandler | undefined,\n input: unknown,\n event?: unknown,\n noContent = false,\n): Route {\n return { handler, input, event, noContent };\n}\n\nfunction handlerFor(\n map: Partial<Record<string, AnyRouteHandler>> | undefined,\n name: string,\n): AnyRouteHandler | undefined {\n return map?.[name];\n}\n\nfunction resolveRoute(\n options: TappifyHandlerOptions,\n method: string,\n segments: string[],\n url: URL,\n payload: unknown,\n): Route | null {\n const [head, name] = segments;\n const bodyInput = readUnknown(payload, 'input') ?? {};\n\n if (head === 'metrics' && method === 'POST') {\n return route(options.metrics, bodyInput);\n }\n if (head === 'tools' && method === 'POST' && name !== undefined) {\n return route(handlerFor(options.tools, name), bodyInput);\n }\n if (head === 'mentions' && method === 'GET' && name !== undefined) {\n return route(handlerFor(options.mentions, name), {\n q: url.searchParams.get('q') ?? '',\n });\n }\n if (head === 'actions' && method === 'POST' && name !== undefined) {\n return route(handlerFor(options.actions, name), bodyInput);\n }\n if (head === 'procedures' && method === 'POST' && name !== undefined) {\n return route(handlerFor(options.procedures, name), bodyInput);\n }\n if (head === 'context' && method === 'GET' && name !== undefined) {\n return route(handlerFor(options.context, name), undefined);\n }\n if (head === 'work' && method === 'POST' && name !== undefined) {\n const operation = WORK_OPERATIONS.find(candidate => candidate === name);\n const handler =\n operation === undefined ? undefined : options.work?.[operation];\n if (operation === 'set_status' && handler === undefined) {\n const input = readRecord(payload, 'input');\n if (readText(input, 'tappifyStatus') === 'resolved') {\n // SDK v2 resolve handlers conventionally read the provider item as id.\n // eslint-disable-next-line @typescript-eslint/no-deprecated -- SDK v2 compatibility adapter.\n return route(options.work?.resolve, {\n ...input,\n id: readUnknown(input, 'externalItemId'),\n });\n }\n }\n return route(handler, bodyInput);\n }\n if (head === 'events' && method === 'POST') {\n const envelope = readRecord(payload, 'event');\n const eventName = readText(envelope, 'name');\n if (eventName === null) {\n throw new TapServerError(\n SERVER_ERROR_CODES.BODY_INVALID,\n 'The events body has no event.name. Post the envelope Tappify sends unchanged, with the event name under \"event\".',\n 400,\n );\n }\n const declared = EVENTS.find(candidate => candidate === eventName);\n return route(\n declared === undefined ? undefined : options.events?.[declared],\n undefined,\n readUnknown(envelope, 'payload') ?? {},\n true,\n );\n }\n\n return null;\n}\n\n/**\n * Builds the one `fetch` handler that answers every route Tappify calls on your\n * server.\n *\n * @remarks\n * It routes the paths under `/tappify`, minus `basePath` when your framework mounts\n * it under a prefix, and hands each matched handler a `TappifyRequest`. Every\n * route but `GET /tappify/health` needs a bearer install token, which it verifies\n * with `verifyTappifyToken`, and an `X-Tappify-Event-Id` header; a missing token is\n * 401 `TAP_TOKEN_MISSING`, a missing id 400 `TAP_EVENT_ID_MISSING`, an unknown path\n * 404 `TAP_ROUTE_UNKNOWN` and a declared name with no handler 404\n * `TAP_HANDLER_MISSING`. A body that is not JSON is 400 `TAP_BODY_INVALID` and one\n * over 1 MB is 413 `TAP_BODY_TOO_LARGE`. A `TapServerError` a handler throws is\n * answered with its own code and status, and a `TapError` other than the two token\n * failures with its own code and 400; anything else becomes 500\n * `TAP_INTERNAL_ERROR` with a fixed message, so an internal failure never reaches\n * the owner. An events route answers 204 with no body, and every other route\n * answers the handler's return value as JSON. Export the options object as well as\n * the handler: `createTestClient` takes the options.\n *\n * @example\n * ```ts\n * import {\n * createTappifyHandler,\n * type TappifyHandlerOptions,\n * } from '@tappify/extension-sdk/server';\n *\n * const handlerOptions: TappifyHandlerOptions = {\n * extensionId: 'starter',\n * health: () => ({ ok: true, version: '0.1.0' }),\n * procedures: {\n * getSummary: () => ({ installs: 1260, delta: 0.087, topKeyword: 'photo editor' }),\n * },\n * };\n *\n * const handler = createTappifyHandler(handlerOptions);\n * ```\n */\nexport function createTappifyHandler(\n options: TappifyHandlerOptions,\n): TappifyFetchHandler {\n const basePath = options.basePath ?? '';\n\n return async function handle(request: Request): Promise<Response> {\n const url = new URL(request.url);\n const path = url.pathname.startsWith(basePath)\n ? url.pathname.slice(basePath.length)\n : url.pathname;\n\n const segments = path.split('/').filter(segment => segment !== '');\n if (segments[0] !== 'tappify') {\n return failure(\n SERVER_ERROR_CODES.ROUTE_UNKNOWN,\n `Nothing is mounted at ${path}. Tappify calls paths under /tappify; set basePath if your framework mounts the handler under a prefix.`,\n 404,\n );\n }\n\n const rest = segments.slice(1);\n\n if (rest[0] === 'health' && request.method === 'GET') {\n const health: TappifyHealth = options.health\n ? await options.health()\n : { ok: true };\n return json(health);\n }\n\n const authorization = request.headers.get('authorization') ?? '';\n const token = authorization.startsWith('Bearer ')\n ? authorization.slice('Bearer '.length)\n : '';\n\n if (token === '') {\n return failure(\n SERVER_ERROR_CODES.TOKEN_MISSING,\n 'This route needs an install token. Tappify sends it as Authorization: Bearer <token>; forward the header unchanged from your framework.',\n 401,\n );\n }\n\n const eventId = request.headers.get('x-tappify-event-id');\n if (eventId === null || eventId === '') {\n return failure(\n SERVER_ERROR_CODES.EVENT_ID_MISSING,\n 'Every Tappify call carries X-Tappify-Event-Id for idempotency. Forward the header unchanged, and use it as the key when you deduplicate.',\n 400,\n );\n }\n\n try {\n const claims = await verifyTappifyToken(token, {\n extensionId: options.extensionId,\n jwksUrl: options.jwksUrl,\n jwks: options.jwks,\n });\n\n const payload = await readBody(request);\n const matched = resolveRoute(options, request.method, rest, url, payload);\n\n if (matched === null) {\n return failure(\n SERVER_ERROR_CODES.ROUTE_UNKNOWN,\n `Tappify has no ${request.method} ${path} route. The paths it calls are listed under \"Vendor server endpoints\" in the reference; check the method and the spelling.`,\n 404,\n );\n }\n\n if (matched.handler === undefined) {\n return failure(\n SERVER_ERROR_CODES.HANDLER_MISSING,\n `No handler is registered for ${request.method} ${path}. Add it to createTappifyHandler(), or remove the contribution from tappify.extension.json.`,\n 404,\n );\n }\n\n const documents = readRecord(payload, 'documents');\n\n const typed: TappifyRequest<unknown, unknown> = {\n install: toInstall(payload, claims),\n context: toContext(payload),\n input: matched.input,\n credentials: toCredentials(payload),\n documents: documents as TappifyDocuments,\n event: matched.event,\n eventId,\n claims,\n request,\n };\n\n const result = await matched.handler(typed);\n return matched.noContent\n ? new Response(null, { status: 204 })\n : json(result);\n } catch (error) {\n if (TapServerError.is(error)) {\n return wireError(error.code, error.message, error.status ?? 400);\n }\n if (TapError.is(error)) {\n const tokenFailure = TOKEN_FAILURES.find(\n candidate => candidate === error.code,\n );\n if (tokenFailure !== undefined) {\n return failure(tokenFailure, error.message, 401);\n }\n return wireError(error.code, error.message, 400);\n }\n return failure(SERVER_ERROR_CODES.INTERNAL_ERROR, INTERNAL_MESSAGE, 500);\n }\n };\n}\n\nexport type {\n ContextBlock,\n MentionItems,\n MetricsInput,\n MetricsResponse,\n TappifyClaims,\n};\n"]}
|