@tappify/extension-sdk 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (98) hide show
  1. package/LICENSE +21 -0
  2. package/dist/chunk-2DBSVKAU.cjs +65 -0
  3. package/dist/chunk-2DBSVKAU.cjs.map +1 -0
  4. package/dist/chunk-3YWVYA42.cjs +142 -0
  5. package/dist/chunk-3YWVYA42.cjs.map +1 -0
  6. package/dist/chunk-4C5Z3CIK.mjs +26 -0
  7. package/dist/chunk-4C5Z3CIK.mjs.map +1 -0
  8. package/dist/chunk-5AAPD5WV.cjs +250 -0
  9. package/dist/chunk-5AAPD5WV.cjs.map +1 -0
  10. package/dist/chunk-7KIKOH5H.cjs +26 -0
  11. package/dist/chunk-7KIKOH5H.cjs.map +1 -0
  12. package/dist/chunk-BDCX6LRQ.mjs +250 -0
  13. package/dist/chunk-BDCX6LRQ.mjs.map +1 -0
  14. package/dist/chunk-CSTYBKZC.mjs +65 -0
  15. package/dist/chunk-CSTYBKZC.mjs.map +1 -0
  16. package/dist/chunk-IGK2AP6J.mjs +142 -0
  17. package/dist/chunk-IGK2AP6J.mjs.map +1 -0
  18. package/dist/chunk-MNDVYPEP.mjs +302 -0
  19. package/dist/chunk-MNDVYPEP.mjs.map +1 -0
  20. package/dist/chunk-OPPYEA7F.cjs +33 -0
  21. package/dist/chunk-OPPYEA7F.cjs.map +1 -0
  22. package/dist/chunk-PYJMJF44.cjs +302 -0
  23. package/dist/chunk-PYJMJF44.cjs.map +1 -0
  24. package/dist/chunk-QHM7XNRK.mjs +33 -0
  25. package/dist/chunk-QHM7XNRK.mjs.map +1 -0
  26. package/dist/chunk-RMUWRESK.cjs +365 -0
  27. package/dist/chunk-RMUWRESK.cjs.map +1 -0
  28. package/dist/chunk-ZT35X7KL.mjs +365 -0
  29. package/dist/chunk-ZT35X7KL.mjs.map +1 -0
  30. package/dist/codegen.cjs +300 -0
  31. package/dist/codegen.cjs.map +1 -0
  32. package/dist/codegen.d.cts +432 -0
  33. package/dist/codegen.d.ts +432 -0
  34. package/dist/codegen.mjs +272 -0
  35. package/dist/codegen.mjs.map +1 -0
  36. package/dist/constants-DJ_KgZpN.d.cts +449 -0
  37. package/dist/constants-DJ_KgZpN.d.ts +449 -0
  38. package/dist/context-Dt6xh_5t.d.cts +30 -0
  39. package/dist/context-Ml7k_V14.d.ts +30 -0
  40. package/dist/errors-COsBh7kd.d.cts +88 -0
  41. package/dist/errors-COsBh7kd.d.ts +88 -0
  42. package/dist/handler-CZIlZ5vu.d.ts +227 -0
  43. package/dist/handler-kU9Jf36K.d.cts +227 -0
  44. package/dist/host.cjs +24 -0
  45. package/dist/host.cjs.map +1 -0
  46. package/dist/host.d.cts +24 -0
  47. package/dist/host.d.ts +24 -0
  48. package/dist/host.mjs +24 -0
  49. package/dist/host.mjs.map +1 -0
  50. package/dist/index.cjs +698 -0
  51. package/dist/index.cjs.map +1 -0
  52. package/dist/index.d.cts +1016 -0
  53. package/dist/index.d.ts +1016 -0
  54. package/dist/index.mjs +698 -0
  55. package/dist/index.mjs.map +1 -0
  56. package/dist/manifest.cjs +872 -0
  57. package/dist/manifest.cjs.map +1 -0
  58. package/dist/manifest.d.cts +432 -0
  59. package/dist/manifest.d.ts +432 -0
  60. package/dist/manifest.mjs +872 -0
  61. package/dist/manifest.mjs.map +1 -0
  62. package/dist/matchers-Cz93AK1n.d.cts +74 -0
  63. package/dist/matchers-Cz93AK1n.d.ts +74 -0
  64. package/dist/server.cjs +210 -0
  65. package/dist/server.cjs.map +1 -0
  66. package/dist/server.d.cts +243 -0
  67. package/dist/server.d.ts +243 -0
  68. package/dist/server.mjs +210 -0
  69. package/dist/server.mjs.map +1 -0
  70. package/dist/styles.css +367 -0
  71. package/dist/testing/index.cjs +887 -0
  72. package/dist/testing/index.cjs.map +1 -0
  73. package/dist/testing/index.d.cts +512 -0
  74. package/dist/testing/index.d.ts +512 -0
  75. package/dist/testing/index.mjs +887 -0
  76. package/dist/testing/index.mjs.map +1 -0
  77. package/dist/testing/jest.cjs +17 -0
  78. package/dist/testing/jest.cjs.map +1 -0
  79. package/dist/testing/jest.d.cts +12 -0
  80. package/dist/testing/jest.d.ts +12 -0
  81. package/dist/testing/jest.mjs +17 -0
  82. package/dist/testing/jest.mjs.map +1 -0
  83. package/dist/testing/vitest.cjs +15 -0
  84. package/dist/testing/vitest.cjs.map +1 -0
  85. package/dist/testing/vitest.d.cts +10 -0
  86. package/dist/testing/vitest.d.ts +10 -0
  87. package/dist/testing/vitest.mjs +15 -0
  88. package/dist/testing/vitest.mjs.map +1 -0
  89. package/dist/types-DDAGLpov.d.ts +715 -0
  90. package/dist/types-DrpQSv_2.d.cts +715 -0
  91. package/dist/vite.cjs +1344 -0
  92. package/dist/vite.cjs.map +1 -0
  93. package/dist/vite.d.cts +434 -0
  94. package/dist/vite.d.ts +434 -0
  95. package/dist/vite.mjs +1314 -0
  96. package/dist/vite.mjs.map +1 -0
  97. package/package.json +268 -0
  98. package/schema/extension-v2.json +1500 -0
@@ -0,0 +1,887 @@
1
+ import {
2
+ installPrefix,
3
+ procedurePrefix
4
+ } from "../chunk-BDCX6LRQ.mjs";
5
+ import {
6
+ TapHostProvider
7
+ } from "../chunk-4C5Z3CIK.mjs";
8
+ import "../chunk-QHM7XNRK.mjs";
9
+ import {
10
+ SERVER_ERROR_CODES,
11
+ createTappifyHandler,
12
+ readRecord,
13
+ readText
14
+ } from "../chunk-ZT35X7KL.mjs";
15
+ import {
16
+ CARD_KEYS,
17
+ QUERY_SCOPES,
18
+ SCOPE_KEYS
19
+ } from "../chunk-MNDVYPEP.mjs";
20
+ import {
21
+ installHostTheme,
22
+ registerTapMatchers,
23
+ tapMatchers
24
+ } from "../chunk-IGK2AP6J.mjs";
25
+ import {
26
+ TapError,
27
+ TapServerError
28
+ } from "../chunk-CSTYBKZC.mjs";
29
+
30
+ // src/testing/client.ts
31
+ import {
32
+ exportJWK,
33
+ generateKeyPair,
34
+ SignJWT
35
+ } from "jose";
36
+
37
+ // src/testing/envelope.ts
38
+ var DEFAULT_FILTERS = {
39
+ range: { from: "2026-09-01", to: "2026-09-08", preset: "7d" },
40
+ platform: "all",
41
+ country: "all"
42
+ };
43
+ function callEnvelope(envelope) {
44
+ const context = {
45
+ projectId: envelope.install.projectId,
46
+ platform: envelope.filters.platform,
47
+ from: envelope.filters.range.from,
48
+ to: envelope.filters.range.to,
49
+ filters: envelope.filters
50
+ };
51
+ return JSON.stringify({
52
+ install: envelope.install,
53
+ context,
54
+ input: envelope.input,
55
+ credentials: envelope.credentials ?? {},
56
+ documents: envelope.documents ?? {},
57
+ event: envelope.event
58
+ });
59
+ }
60
+ function parseJson(text) {
61
+ try {
62
+ return { parsed: true, value: JSON.parse(text) };
63
+ } catch {
64
+ return { parsed: false, value: void 0 };
65
+ }
66
+ }
67
+ async function readEnvelopeResponse(response, call) {
68
+ const text = await response.text();
69
+ const body = parseJson(text);
70
+ const status = String(response.status);
71
+ if (response.ok) {
72
+ if (body.parsed) return body.value;
73
+ if (text.trim() === "") return void 0;
74
+ throw new TapServerError(
75
+ SERVER_ERROR_CODES.INTERNAL_ERROR,
76
+ `The extension server answered ${status} for ${call} with a body that is not JSON. Every Tappify route answers JSON, or 204 with no body.`,
77
+ response.status
78
+ );
79
+ }
80
+ const error = readRecord(body.value, "error");
81
+ throw new TapServerError(
82
+ readText(error, "code") ?? SERVER_ERROR_CODES.INTERNAL_ERROR,
83
+ readText(error, "message") ?? `The extension server answered ${status} for ${call} without a JSON error body.`,
84
+ response.status
85
+ );
86
+ }
87
+
88
+ // src/testing/client.ts
89
+ var TEST_KID = "tappify-test-key";
90
+ var TEST_ISSUER = "https://api.tappify.ai";
91
+ var keys = null;
92
+ function ensureKeys() {
93
+ keys ?? (keys = (async () => {
94
+ const pair = await generateKeyPair("RS256", { extractable: true });
95
+ const publicJwk = await exportJWK(pair.publicKey);
96
+ return {
97
+ privateKey: pair.privateKey,
98
+ jwks: {
99
+ keys: [{ ...publicJwk, kid: TEST_KID, alg: "RS256", use: "sig" }]
100
+ }
101
+ };
102
+ })());
103
+ return keys;
104
+ }
105
+ async function testJwks() {
106
+ return (await ensureKeys()).jwks;
107
+ }
108
+ async function signTestToken(claims = {}, options = {}) {
109
+ const { privateKey } = await ensureKeys();
110
+ const extensionId = options.extensionId ?? claims.extensionId ?? "starter";
111
+ return new SignJWT({
112
+ installId: claims.installId ?? "ins_test",
113
+ extensionId,
114
+ projectId: claims.projectId ?? "prj_north",
115
+ organizationId: claims.organizationId ?? "org_test",
116
+ scopes: claims.scopes ?? [],
117
+ ...claims.userId === void 0 ? {} : { userId: claims.userId }
118
+ }).setProtectedHeader({ alg: "RS256", kid: TEST_KID }).setIssuer(TEST_ISSUER).setAudience(`ext:${extensionId}`).setIssuedAt().setExpirationTime(options.expiresIn ?? "5m").sign(privateKey);
119
+ }
120
+ var HEALTH_PATH = "/tappify/health";
121
+ function createTestClient(target, defaults = {}) {
122
+ if (typeof target === "function") {
123
+ throw new TapError(
124
+ "TAP_TEST_TARGET_INVALID",
125
+ "createTestClient() was handed a built handler instead of the options object. A handler from createTappifyHandler() verifies install tokens against the live Tappify key set, so every call the test client makes would come back 401. Pass the options themselves \u2014 createTestClient(handlerOptions) \u2014 or, when you need the handler, build it with createTappifyHandler({ ...handlerOptions, jwks: await testJwks() }) and drive it through createTapMock({ handler, extensionId: handlerOptions.extensionId }) so the test token carries the audience the handler expects."
126
+ );
127
+ }
128
+ const extensionId = target.extensionId;
129
+ const handlerPromise = testJwks().then(
130
+ (jwks) => createTappifyHandler({ ...target, jwks })
131
+ );
132
+ const install = {
133
+ id: "ins_test",
134
+ extensionId,
135
+ projectId: "prj_north",
136
+ organizationId: "org_test",
137
+ ...defaults.install
138
+ };
139
+ const filters = defaults.filters ?? DEFAULT_FILTERS;
140
+ const origin = defaults.origin ?? "https://vendor.test";
141
+ let sent = 0;
142
+ function envelope(input, event) {
143
+ return callEnvelope({
144
+ install,
145
+ filters,
146
+ input,
147
+ credentials: defaults.credentials,
148
+ documents: defaults.documents,
149
+ event
150
+ });
151
+ }
152
+ async function send(method, path, body) {
153
+ const handler = await handlerPromise;
154
+ sent += 1;
155
+ const headers = new Headers({
156
+ "content-type": "application/json",
157
+ "x-tappify-event-id": defaults.eventId ?? `evt_${path}_${String(sent)}`
158
+ });
159
+ if (path !== HEALTH_PATH) {
160
+ const token = await signTestToken(
161
+ {
162
+ installId: install.id,
163
+ extensionId: install.extensionId,
164
+ projectId: install.projectId,
165
+ organizationId: install.organizationId,
166
+ scopes: defaults.scopes ?? []
167
+ },
168
+ { extensionId }
169
+ );
170
+ headers.set("authorization", `Bearer ${token}`);
171
+ }
172
+ return handler(
173
+ new Request(`${origin}${path}`, {
174
+ method,
175
+ headers,
176
+ ...body === void 0 ? {} : { body }
177
+ })
178
+ );
179
+ }
180
+ async function call(method, path, body) {
181
+ return readEnvelopeResponse(
182
+ await send(method, path, body),
183
+ `${method} ${path}`
184
+ );
185
+ }
186
+ return {
187
+ health: async () => await call("GET", HEALTH_PATH),
188
+ metrics: async (input) => await call(
189
+ "POST",
190
+ "/tappify/metrics",
191
+ envelope(input)
192
+ ),
193
+ tool: (id, input) => call("POST", `/tappify/tools/${id}`, envelope(input)),
194
+ mention: async (id, query) => await call(
195
+ "GET",
196
+ `/tappify/mentions/${id}?q=${encodeURIComponent(query)}`
197
+ ),
198
+ action: (id, input) => call("POST", `/tappify/actions/${id}`, envelope(input)),
199
+ procedure: (name, input = {}) => call("POST", `/tappify/procedures/${name}`, envelope(input)),
200
+ context: async (id) => await call("GET", `/tappify/context/${id}`),
201
+ event: async (name, payload) => {
202
+ await call("POST", "/tappify/events", envelope({}, { name, payload }));
203
+ },
204
+ work: (operation, input) => call("POST", `/tappify/work/${operation}`, envelope(input)),
205
+ raw: (method, path, body) => send(method, path, body === void 0 ? void 0 : JSON.stringify(body))
206
+ };
207
+ }
208
+
209
+ // src/testing/fixtures.ts
210
+ var fixtureApps = [
211
+ {
212
+ id: "app_ios",
213
+ name: "Northlight",
214
+ platform: "ios",
215
+ storeId: "1490000001",
216
+ bundleId: "ai.tappify.northlight"
217
+ },
218
+ {
219
+ id: "app_android",
220
+ name: "Northlight",
221
+ platform: "android",
222
+ storeId: "ai.tappify.northlight",
223
+ bundleId: "ai.tappify.northlight"
224
+ }
225
+ ];
226
+ var fixtureReleases = [
227
+ {
228
+ id: "rel_42",
229
+ version: "4.2.0",
230
+ build: "118",
231
+ platform: "ios",
232
+ state: "ready_for_sale",
233
+ shippedAt: "2026-09-01T09:00:00.000Z"
234
+ },
235
+ {
236
+ id: "rel_41",
237
+ version: "4.1.0",
238
+ build: "107",
239
+ platform: "ios",
240
+ state: "ready_for_sale",
241
+ shippedAt: "2026-08-11T09:00:00.000Z"
242
+ },
243
+ {
244
+ id: "rel_a42",
245
+ version: "4.2.0",
246
+ build: "4200",
247
+ platform: "android",
248
+ state: "published",
249
+ shippedAt: "2026-09-02T09:00:00.000Z"
250
+ }
251
+ ];
252
+ var fixtureKeywords = [
253
+ {
254
+ id: "kw_1",
255
+ term: "photo editor",
256
+ platform: "ios",
257
+ country: "US",
258
+ position: 4,
259
+ popularity: 6.6
260
+ },
261
+ {
262
+ id: "kw_2",
263
+ term: "collage maker",
264
+ platform: "ios",
265
+ country: "US",
266
+ position: 11,
267
+ popularity: 5.3
268
+ },
269
+ {
270
+ id: "kw_3",
271
+ term: "photo editor",
272
+ platform: "android",
273
+ country: "US",
274
+ position: 7,
275
+ popularity: 6
276
+ }
277
+ ];
278
+ var fixtureListings = [
279
+ {
280
+ platform: "ios",
281
+ country: "US",
282
+ locale: "en-US",
283
+ title: "Northlight \u2014 Photo Editor",
284
+ subtitle: "Edit, collage, share",
285
+ description: "Northlight turns a camera roll into a finished post.",
286
+ keywords: ["photo", "editor", "collage"],
287
+ updatedAt: "2026-08-28T12:00:00.000Z"
288
+ }
289
+ ];
290
+ var fixtureProject = {
291
+ project: {
292
+ id: "prj_north",
293
+ name: "Northlight",
294
+ platforms: ["ios", "android"]
295
+ },
296
+ apps: fixtureApps,
297
+ releases: fixtureReleases
298
+ };
299
+ var SERIES_VALUES = {
300
+ downloads: [1180, 1240, 1310, 1290, 1402, 1512, 1488],
301
+ impressions: [21400, 22150, 22980, 22110, 24020, 25510, 25010],
302
+ page_views: [8120, 8340, 8710, 8290, 9110, 9640, 9420],
303
+ conversion_rate: [0.145, 0.149, 0.15, 0.156, 0.152, 0.157, 0.158]
304
+ };
305
+ function week() {
306
+ return [
307
+ "2026-09-01",
308
+ "2026-09-02",
309
+ "2026-09-03",
310
+ "2026-09-04",
311
+ "2026-09-05",
312
+ "2026-09-06",
313
+ "2026-09-07"
314
+ ];
315
+ }
316
+ function fixtureSeries(metric) {
317
+ const points = week().map((at, index) => ({
318
+ at,
319
+ value: SERIES_VALUES[metric][index]
320
+ }));
321
+ return {
322
+ metric,
323
+ unit: metric === "conversion_rate" ? "ratio" : "count",
324
+ points
325
+ };
326
+ }
327
+ var fixtureReviews = {
328
+ total: 3,
329
+ averageRating: 3.7,
330
+ reviews: [
331
+ {
332
+ id: "rev_1",
333
+ platform: "ios",
334
+ country: "US",
335
+ rating: 5,
336
+ title: "Fast and clean",
337
+ body: "The new collage layouts are exactly what I wanted.",
338
+ author: "kbrady",
339
+ submittedAt: "2026-09-05T18:22:00.000Z",
340
+ replied: false
341
+ },
342
+ {
343
+ id: "rev_2",
344
+ platform: "ios",
345
+ country: "GB",
346
+ rating: 2,
347
+ title: "Crashes on export",
348
+ body: "Since 4.2 the export button closes the app.",
349
+ author: null,
350
+ submittedAt: "2026-09-04T08:10:00.000Z",
351
+ replied: true
352
+ },
353
+ {
354
+ id: "rev_3",
355
+ platform: "android",
356
+ country: "DE",
357
+ rating: 4,
358
+ title: null,
359
+ body: "Gute App, aber der Export dauert lange.",
360
+ author: "mgruber",
361
+ submittedAt: "2026-09-03T16:45:00.000Z",
362
+ replied: false
363
+ }
364
+ ]
365
+ };
366
+ var fixtureCrashes = {
367
+ crashFreeRate: 99.42,
368
+ issues: [
369
+ {
370
+ id: "crash_1",
371
+ title: "ExportSession.finish \u2014 index out of range",
372
+ count: 214,
373
+ build: "118",
374
+ firstSeenAt: "2026-09-01T11:02:00.000Z",
375
+ lastSeenAt: "2026-09-07T21:40:00.000Z"
376
+ },
377
+ {
378
+ id: "crash_2",
379
+ title: "PhotoLibrary.load \u2014 permission revoked",
380
+ count: 37,
381
+ build: "118",
382
+ firstSeenAt: "2026-09-02T07:15:00.000Z",
383
+ lastSeenAt: "2026-09-06T13:05:00.000Z"
384
+ }
385
+ ]
386
+ };
387
+ var fixtureRevenue = {
388
+ currency: "USD",
389
+ mrr: 41200,
390
+ refunds: 18,
391
+ points: week().map((at, index) => ({
392
+ at,
393
+ value: [1310, 1355, 1401, 1388, 1470, 1525, 1502][index]
394
+ }))
395
+ };
396
+ var fixtures = {
397
+ project: fixtureProject,
398
+ apps: fixtureApps,
399
+ releases: fixtureReleases,
400
+ keywords: fixtureKeywords,
401
+ listings: fixtureListings,
402
+ reviews: fixtureReviews,
403
+ crashes: fixtureCrashes,
404
+ revenue: fixtureRevenue,
405
+ series: fixtureSeries
406
+ };
407
+ function answerFixtureQuery(query) {
408
+ switch (query.kind) {
409
+ case "project":
410
+ return structuredClone(fixtureProject);
411
+ case "series":
412
+ return structuredClone(fixtureSeries(query.metric));
413
+ case "keywords":
414
+ return structuredClone({ keywords: fixtureKeywords });
415
+ case "listing":
416
+ return structuredClone({ listings: fixtureListings });
417
+ case "reviews":
418
+ return structuredClone(fixtureReviews);
419
+ case "crashes":
420
+ return structuredClone(fixtureCrashes);
421
+ case "revenue":
422
+ return structuredClone(fixtureRevenue);
423
+ }
424
+ }
425
+
426
+ // src/testing/mock.ts
427
+ var SINGLETON_ID = "singleton";
428
+ var PROBE_KEYS = /* @__PURE__ */ new Set([
429
+ "then",
430
+ "toJSON",
431
+ "$$typeof",
432
+ "constructor",
433
+ "prototype",
434
+ "valueOf",
435
+ "toString",
436
+ "inspect",
437
+ "asymmetricMatch",
438
+ "nodeType"
439
+ ]);
440
+ function unknownCollectionError(collection) {
441
+ return new TapError(
442
+ "TAP_UNKNOWN_COLLECTION",
443
+ `This mock exposes no storage collection called "${collection}". createTapMock({ storage: \u2026 }) was given an explicit map, so only the collections it names exist. Add "${collection}" to that map, or drop the storage option to expose every collection.`
444
+ );
445
+ }
446
+ function singletonView(handle) {
447
+ return {
448
+ get: () => handle.get(),
449
+ set: (document2) => handle.set(document2),
450
+ patch: (partial) => handle.patch(partial)
451
+ };
452
+ }
453
+ function collectionView(handle) {
454
+ return {
455
+ list: (listOptions) => handle.list(listOptions),
456
+ get: (id) => handle.get(id),
457
+ put: (id, document2) => handle.put(id, document2),
458
+ delete: (id) => handle.delete(id)
459
+ };
460
+ }
461
+ function scopeError(scope) {
462
+ return new TapError(
463
+ "TAP_SCOPE_MISSING",
464
+ `This install has not granted ${scope}. Declare it under "scopes" in tappify.extension.json, or pass scopes: ["${scope}"] to renderWithTap() to test the granted path.`
465
+ );
466
+ }
467
+ function asObject(value) {
468
+ return typeof value === "object" && value !== null ? value : {};
469
+ }
470
+ function createTapMock(options = {}) {
471
+ const granted = new Set(options.scopes ?? SCOPE_KEYS);
472
+ const listeners = /* @__PURE__ */ new Map();
473
+ const eventHandlers = /* @__PURE__ */ new Map();
474
+ const state = /* @__PURE__ */ new Map();
475
+ const store = /* @__PURE__ */ new Map();
476
+ const portal = document.createElement("div");
477
+ portal.dataset.tapPortal = "true";
478
+ const calls = {
479
+ navigations: [],
480
+ searches: [],
481
+ toasts: [],
482
+ confirms: [],
483
+ actions: [],
484
+ telemetry: [],
485
+ chats: [],
486
+ externals: [],
487
+ downloads: [],
488
+ copies: [],
489
+ cards: [],
490
+ expands: 0,
491
+ settingsOpens: 0
492
+ };
493
+ const emitChange = (key) => {
494
+ for (const listener of listeners.get(key) ?? []) listener();
495
+ };
496
+ const subscribe = (key, listener) => {
497
+ const set = listeners.get(key) ?? /* @__PURE__ */ new Set();
498
+ set.add(listener);
499
+ listeners.set(key, set);
500
+ return () => {
501
+ set.delete(listener);
502
+ };
503
+ };
504
+ const withStorage = (run) => granted.has("storage:write") ? Promise.resolve(run()) : Promise.reject(scopeError("storage:write"));
505
+ const bucket = (collection) => {
506
+ const existing = store.get(collection);
507
+ if (existing) return existing;
508
+ const created = [];
509
+ store.set(collection, created);
510
+ return created;
511
+ };
512
+ function buildHandle(collection) {
513
+ const find = (id) => bucket(collection).find((entry) => entry.id === id);
514
+ const write = (id, data) => {
515
+ const items = bucket(collection);
516
+ const index = items.findIndex((entry) => entry.id === id);
517
+ if (index === -1) items.push({ id, data });
518
+ else items[index] = { id, data };
519
+ emitChange(`storage:${collection}`);
520
+ };
521
+ return {
522
+ get: (id) => withStorage(() => find(id ?? SINGLETON_ID)?.data ?? null),
523
+ set: (document2) => withStorage(() => {
524
+ write(SINGLETON_ID, document2);
525
+ }),
526
+ patch: (partial) => withStorage(() => {
527
+ const merged = { ...asObject(find(SINGLETON_ID)?.data), ...partial };
528
+ write(SINGLETON_ID, merged);
529
+ return merged;
530
+ }),
531
+ list: () => withStorage(() => ({
532
+ items: bucket(collection).filter((entry) => entry.id !== SINGLETON_ID).map((entry) => ({ ...asObject(entry.data), id: entry.id }))
533
+ })),
534
+ put: (id, document2) => withStorage(() => {
535
+ write(id, document2);
536
+ }),
537
+ delete: (id) => withStorage(() => {
538
+ const items = bucket(collection);
539
+ const index = items.findIndex((entry) => entry.id === id);
540
+ if (index !== -1) {
541
+ items.splice(index, 1);
542
+ emitChange(`storage:${collection}`);
543
+ }
544
+ })
545
+ };
546
+ }
547
+ const handles = /* @__PURE__ */ new Map();
548
+ const kinds = options.storage;
549
+ const exposeHandle = (collection) => {
550
+ const kind = kinds?.[collection];
551
+ if (kinds !== void 0 && kind === void 0) {
552
+ throw unknownCollectionError(collection);
553
+ }
554
+ const handle = buildHandle(collection);
555
+ if (kind === "singleton") return singletonView(handle);
556
+ if (kind === "collection") return collectionView(handle);
557
+ return handle;
558
+ };
559
+ const handleFor = (collection) => {
560
+ if (PROBE_KEYS.has(collection)) return void 0;
561
+ const existing = handles.get(collection);
562
+ if (existing) return existing;
563
+ const created = exposeHandle(collection);
564
+ handles.set(collection, created);
565
+ return created;
566
+ };
567
+ const storageTarget = {};
568
+ const storage = new Proxy(storageTarget, {
569
+ get: (_target, property) => typeof property === "string" ? handleFor(property) : void 0,
570
+ has: () => true,
571
+ ownKeys: () => [...store.keys()],
572
+ getOwnPropertyDescriptor: () => ({ enumerable: true, configurable: true })
573
+ });
574
+ let calledProcedures = 0;
575
+ let signedToken = null;
576
+ function handlerToken() {
577
+ if (options.handlerToken !== void 0) {
578
+ return Promise.resolve(options.handlerToken);
579
+ }
580
+ signedToken ?? (signedToken = signTestToken(
581
+ {
582
+ installId: tap.extension.installId,
583
+ extensionId: tap.extension.id,
584
+ projectId: tap.extension.projectId,
585
+ organizationId: tap.auth.organizationId,
586
+ scopes: [...granted]
587
+ },
588
+ { extensionId: tap.extension.id }
589
+ ));
590
+ return signedToken;
591
+ }
592
+ async function callProcedure(name, input) {
593
+ const local = options.server?.[name];
594
+ if (local) return local(input);
595
+ if (options.handler) {
596
+ const path = `/tappify/procedures/${name}`;
597
+ calledProcedures += 1;
598
+ const response = await options.handler(
599
+ new Request(`https://vendor.test${path}`, {
600
+ method: "POST",
601
+ headers: {
602
+ "content-type": "application/json",
603
+ "x-tappify-event-id": `evt_${name}_${String(calledProcedures)}`,
604
+ authorization: `Bearer ${await handlerToken()}`
605
+ },
606
+ body: callEnvelope({
607
+ install: {
608
+ id: tap.extension.installId,
609
+ extensionId: tap.extension.id,
610
+ projectId: tap.extension.projectId,
611
+ organizationId: tap.auth.organizationId
612
+ },
613
+ filters: tap.filters,
614
+ input
615
+ })
616
+ })
617
+ );
618
+ return readEnvelopeResponse(response, `POST ${path}`);
619
+ }
620
+ throw new TapError(
621
+ "TAP_UNKNOWN_PROCEDURE",
622
+ `No handler is registered for the procedure "${name}". Pass server: { ${name}: input => \u2026 } or handler: <your createTappifyHandler handler> to createTapMock().`
623
+ );
624
+ }
625
+ const serverTarget = {};
626
+ const tap = {
627
+ extension: {
628
+ id: options.extensionId ?? "starter",
629
+ name: options.extensionName ?? "Starter",
630
+ installId: options.installId ?? "ins_test",
631
+ projectId: "prj_north"
632
+ },
633
+ env: "dev",
634
+ host: { version: "test", sdkMajors: [2] },
635
+ auth: {
636
+ token: "test-token",
637
+ user: { id: "usr_test", role: "admin" },
638
+ organizationId: "org_test",
639
+ scopes: [...granted],
640
+ can: (scope) => granted.has(scope)
641
+ },
642
+ project: {
643
+ id: "prj_north",
644
+ name: "Northlight",
645
+ platforms: ["ios", "android"],
646
+ apps: [],
647
+ releases: [],
648
+ keywords: [],
649
+ ...options.project
650
+ },
651
+ filters: { ...DEFAULT_FILTERS, ...options.filters },
652
+ theme: { mode: options.theme ?? "light" },
653
+ locale: "en-US",
654
+ timezone: "UTC",
655
+ format: {
656
+ number: (value, formatOptions) => new Intl.NumberFormat("en-US", formatOptions).format(value),
657
+ currency: (value, code) => new Intl.NumberFormat("en-US", {
658
+ style: "currency",
659
+ currency: code
660
+ }).format(value),
661
+ date: (value) => new Date(value).toISOString().slice(0, 10),
662
+ relative: (value) => new Date(value).toISOString()
663
+ },
664
+ nav: {
665
+ push: (path) => calls.navigations.push(path),
666
+ setSearch: (params) => calls.searches.push(params),
667
+ openSettings: () => {
668
+ calls.settingsOpens += 1;
669
+ },
670
+ openChat: (prompt, context) => calls.chats.push({ prompt, context }),
671
+ openExternal: (url) => calls.externals.push(url)
672
+ },
673
+ data: {
674
+ query: (query) => {
675
+ const scope = QUERY_SCOPES[query.kind];
676
+ if (!granted.has(scope)) return Promise.reject(scopeError(scope));
677
+ return Promise.resolve(answerFixtureQuery(query));
678
+ },
679
+ subscribe: (event, handler) => {
680
+ const key = String(event);
681
+ const set = eventHandlers.get(key) ?? /* @__PURE__ */ new Set();
682
+ const wrapped = (payload) => {
683
+ handler(payload);
684
+ };
685
+ set.add(wrapped);
686
+ eventHandlers.set(key, set);
687
+ return () => {
688
+ set.delete(wrapped);
689
+ };
690
+ }
691
+ },
692
+ server: new Proxy(serverTarget, {
693
+ get: (_target, property) => (input) => callProcedure(String(property), input),
694
+ has: () => true
695
+ }),
696
+ state: {
697
+ get: (key) => {
698
+ const value = state.get(key);
699
+ return value === void 0 ? void 0 : value;
700
+ },
701
+ set: (key, value) => {
702
+ state.set(key, value);
703
+ emitChange(`state:${key}`);
704
+ },
705
+ subscribe: (key, handler) => subscribe(`state:${key}`, handler)
706
+ },
707
+ ui: {
708
+ size: options.size ?? "slot",
709
+ toast: (message, tone = "neutral") => calls.toasts.push({ message, tone }),
710
+ confirm: (confirmOptions) => {
711
+ calls.confirms.push(confirmOptions);
712
+ return Promise.resolve(options.confirmAnswer ?? true);
713
+ },
714
+ openInChat: (card) => calls.cards.push(card),
715
+ expand: () => {
716
+ calls.expands += 1;
717
+ },
718
+ download: (blob, filename) => calls.downloads.push({ filename, size: blob.size }),
719
+ copy: (text) => {
720
+ calls.copies.push(text);
721
+ return Promise.resolve();
722
+ }
723
+ },
724
+ context: options.context ?? {},
725
+ params: options.params ?? {},
726
+ storage,
727
+ telemetry: {
728
+ event: (name, props) => calls.telemetry.push({ name, props })
729
+ },
730
+ actions: {
731
+ run: (actionId, input) => {
732
+ calls.actions.push({ actionId, input });
733
+ return Promise.resolve({
734
+ id: `run_${String(calls.actions.length)}`,
735
+ actionId,
736
+ status: "pending",
737
+ requestedAt: (/* @__PURE__ */ new Date(0)).toISOString()
738
+ });
739
+ }
740
+ },
741
+ invalidate: () => void 0,
742
+ __portal: portal,
743
+ __subscribe: subscribe
744
+ };
745
+ return {
746
+ tap,
747
+ calls,
748
+ portal,
749
+ documents: (collection, documentId) => bucket(collection).filter((entry) => documentId === void 0 || entry.id === documentId).map((entry) => entry.data),
750
+ emit: (event, payload) => {
751
+ for (const handler of eventHandlers.get(String(event)) ?? []) {
752
+ handler(payload);
753
+ }
754
+ },
755
+ setFilters: (filters) => {
756
+ tap.filters = filters;
757
+ emitChange("filters");
758
+ },
759
+ setSize: (size) => {
760
+ tap.ui = { ...tap.ui, size };
761
+ emitChange("size");
762
+ },
763
+ setTheme: (mode) => {
764
+ tap.theme = { mode };
765
+ emitChange("theme");
766
+ },
767
+ setParams: (params) => {
768
+ tap.params = params;
769
+ emitChange("params");
770
+ }
771
+ };
772
+ }
773
+
774
+ // src/testing/render.tsx
775
+ import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
776
+ import {
777
+ render
778
+ } from "@testing-library/react";
779
+ import { jsx } from "react/jsx-runtime";
780
+ function renderWithTap(ui, options = {}) {
781
+ const mock = options.mock ?? createTapMock(options);
782
+ const queryClient = options.queryClient ?? new QueryClient({
783
+ defaultOptions: { queries: { retry: false, gcTime: 0 } }
784
+ });
785
+ const installId = mock.tap.extension.installId;
786
+ mock.tap.invalidate = (name) => {
787
+ void queryClient.invalidateQueries({
788
+ queryKey: name === void 0 ? installPrefix(installId) : procedurePrefix(installId, name)
789
+ });
790
+ };
791
+ function Wrapper({ children }) {
792
+ return /* @__PURE__ */ jsx(QueryClientProvider, { client: queryClient, children: /* @__PURE__ */ jsx(TapHostProvider, { tap: mock.tap, children }) });
793
+ }
794
+ const result = render(ui, { wrapper: Wrapper, container: options.container });
795
+ result.baseElement.append(mock.portal);
796
+ return { ...result, tap: mock.tap, mock, queryClient };
797
+ }
798
+
799
+ // src/testing/validators.ts
800
+ function record(value) {
801
+ return typeof value === "object" && value !== null ? value : null;
802
+ }
803
+ function list(value) {
804
+ return Array.isArray(value) ? value : null;
805
+ }
806
+ function isPoint(value) {
807
+ const pair = list(value);
808
+ return pair !== null && pair.length === 2 && typeof pair[0] === "string" && typeof pair[1] === "number";
809
+ }
810
+ function validateMetricsResponse(response, metrics) {
811
+ const series = list(record(response)?.series);
812
+ if (series === null) {
813
+ return [
814
+ 'A metrics response is { "series": [...] }. Return one entry per declared metric, even when it has no points.'
815
+ ];
816
+ }
817
+ const issues = [];
818
+ const declared = new Map(metrics.map((metric) => [metric.key, metric]));
819
+ for (const [index, entry] of series.entries()) {
820
+ const item = record(entry);
821
+ const key = typeof item?.metric === "string" ? item.metric : null;
822
+ if (item === null || key === null) {
823
+ issues.push(`series[${String(index)}] has no "metric" key.`);
824
+ continue;
825
+ }
826
+ const metric = declared.get(key);
827
+ if (metric === void 0) {
828
+ issues.push(
829
+ `series[${String(index)}] reports "${key}", which is not declared under contributes.connector.metrics.`
830
+ );
831
+ continue;
832
+ }
833
+ if (item.unit !== metric.unit) {
834
+ issues.push(
835
+ `series[${String(index)}] reports unit "${String(item.unit)}" but "${key}" is declared as "${metric.unit}".`
836
+ );
837
+ }
838
+ const points = list(item.points);
839
+ if (points === null) {
840
+ issues.push(`series[${String(index)}] has no "points" array.`);
841
+ continue;
842
+ }
843
+ for (const [pointIndex, point] of points.entries()) {
844
+ if (!isPoint(point)) {
845
+ issues.push(
846
+ `series[${String(index)}].points[${String(pointIndex)}] must be a two-element [timestamp, value] pair: a string and a number.`
847
+ );
848
+ }
849
+ }
850
+ }
851
+ return issues;
852
+ }
853
+ function validateToolResponse(value, returns) {
854
+ const body = record(value);
855
+ if (body === null) {
856
+ return [
857
+ `A "${returns}" tool returns an object the host draws as a card, not ${typeof value}.`
858
+ ];
859
+ }
860
+ return CARD_KEYS[returns].filter((key) => !(key in body)).map(
861
+ (key) => `A "${returns}" tool response needs a "${key}" key; the host draws the card from it.`
862
+ );
863
+ }
864
+ export {
865
+ answerFixtureQuery,
866
+ createTapMock,
867
+ createTestClient,
868
+ fixtureApps,
869
+ fixtureCrashes,
870
+ fixtureKeywords,
871
+ fixtureListings,
872
+ fixtureProject,
873
+ fixtureReleases,
874
+ fixtureRevenue,
875
+ fixtureReviews,
876
+ fixtureSeries,
877
+ fixtures,
878
+ installHostTheme,
879
+ registerTapMatchers,
880
+ renderWithTap,
881
+ signTestToken,
882
+ tapMatchers,
883
+ testJwks,
884
+ validateMetricsResponse,
885
+ validateToolResponse
886
+ };
887
+ //# sourceMappingURL=index.mjs.map