@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,872 @@
1
+ import {
2
+ CARD_KEYS,
3
+ CATEGORIES,
4
+ CHARTS,
5
+ EVENTS,
6
+ KINDS,
7
+ PAGES,
8
+ PAGE_KEYS,
9
+ QUERY_SCOPES,
10
+ SCOPES,
11
+ SCOPE_KEYS,
12
+ SERVED_SDK_MAJORS,
13
+ SLOTS,
14
+ TABLES,
15
+ TABLE_KEYS,
16
+ findPage,
17
+ findScope,
18
+ findTable,
19
+ slotsForPage
20
+ } from "./chunk-MNDVYPEP.mjs";
21
+
22
+ // src/manifest/define.ts
23
+ var EXPOSE_FOLDER = {
24
+ page: "pages",
25
+ tab: "tabs",
26
+ widget: "widgets",
27
+ rowAction: "rowActions"
28
+ };
29
+ function defineManifest(manifest) {
30
+ return manifest;
31
+ }
32
+ function exposeName(kind, id) {
33
+ if (kind === "settings") return "./settings";
34
+ return `./${EXPOSE_FOLDER[kind]}/${id ?? ""}`;
35
+ }
36
+ function remoteName(extensionId) {
37
+ return `ext_${extensionId.replace(/-/g, "_")}`;
38
+ }
39
+ function uiContributions(manifest) {
40
+ const contributes = manifest.contributes;
41
+ const refs = [];
42
+ for (const page of contributes.pages ?? []) {
43
+ refs.push({
44
+ kind: "page",
45
+ id: page.id,
46
+ entry: page.entry,
47
+ expose: exposeName("page", page.id)
48
+ });
49
+ }
50
+ for (const tab of contributes.tabs ?? []) {
51
+ refs.push({
52
+ kind: "tab",
53
+ id: tab.id,
54
+ entry: tab.entry,
55
+ expose: exposeName("tab", tab.id)
56
+ });
57
+ }
58
+ for (const widget of contributes.widgets ?? []) {
59
+ refs.push({
60
+ kind: "widget",
61
+ id: widget.id,
62
+ entry: widget.entry,
63
+ expose: exposeName("widget", widget.id)
64
+ });
65
+ }
66
+ for (const rowAction of contributes.rowActions ?? []) {
67
+ refs.push({
68
+ kind: "rowAction",
69
+ id: rowAction.id,
70
+ entry: rowAction.entry,
71
+ expose: exposeName("rowAction", rowAction.id)
72
+ });
73
+ }
74
+ if (contributes.settings) {
75
+ refs.push({
76
+ kind: "settings",
77
+ id: "settings",
78
+ entry: contributes.settings.entry,
79
+ expose: exposeName("settings")
80
+ });
81
+ }
82
+ return refs;
83
+ }
84
+
85
+ // src/manifest/schema.ts
86
+ import { z } from "zod";
87
+ var SLUG = /^[a-z][a-z0-9-]{2,39}$/;
88
+ var CONTRIBUTION_ID = /^[a-z][a-z0-9-]{1,39}$/;
89
+ var SNAKE_CASE = /^[a-z][a-z0-9_]*$/;
90
+ var PROCEDURE_NAME = /^[a-zA-Z][a-zA-Z0-9_-]{1,59}$/;
91
+ var DOMAIN_EVENT = /^[a-z][a-z0-9_]*\.[a-z][a-z0-9_]*$/;
92
+ var jsonSchema = z.record(z.string(), z.unknown()).describe(
93
+ "A JSON Schema object. `tappify extension types` turns it into a TypeScript type."
94
+ );
95
+ var AI_INPUT_SCHEMA_MAX_BYTES = 16384;
96
+ var AI_SCHEMA_TEXT_MAX = 500;
97
+ var AI_SCHEMA_TEXT_KEYS = /* @__PURE__ */ new Set(["title", "description"]);
98
+ var AI_SCHEMA_MAX_DEPTH = 12;
99
+ function overlongSchemaText(value, depth = 0) {
100
+ if (depth > AI_SCHEMA_MAX_DEPTH) return null;
101
+ if (Array.isArray(value)) {
102
+ for (const entry of value) {
103
+ const found = overlongSchemaText(entry, depth + 1);
104
+ if (found !== null) return found;
105
+ }
106
+ return null;
107
+ }
108
+ if (typeof value !== "object" || value === null) return null;
109
+ for (const [key, entry] of Object.entries(value)) {
110
+ if (AI_SCHEMA_TEXT_KEYS.has(key) && typeof entry === "string" && entry.length > AI_SCHEMA_TEXT_MAX) {
111
+ return key;
112
+ }
113
+ const found = overlongSchemaText(entry, depth + 1);
114
+ if (found !== null) return found;
115
+ }
116
+ return null;
117
+ }
118
+ var aiInputSchema = jsonSchema.superRefine((value, ctx) => {
119
+ const size = new TextEncoder().encode(JSON.stringify(value)).length;
120
+ if (size > AI_INPUT_SCHEMA_MAX_BYTES) {
121
+ ctx.addIssue({
122
+ code: "custom",
123
+ message: `This input schema serialises to ${size} bytes; Tappify accepts at most ${AI_INPUT_SCHEMA_MAX_BYTES}. Describe the arguments in fewer words, or take fewer of them.`
124
+ });
125
+ }
126
+ const overlong = overlongSchemaText(value);
127
+ if (overlong !== null) {
128
+ ctx.addIssue({
129
+ code: "custom",
130
+ message: `A "${overlong}" inside this input schema is longer than ${AI_SCHEMA_TEXT_MAX} characters. The assistant reads it like the tool description, which is bounded the same way.`
131
+ });
132
+ }
133
+ });
134
+ var httpsUrl = z.url().startsWith("https://");
135
+ var entryPath = z.string().min(1).max(200).describe(
136
+ "The source file that renders this contribution, as a path relative to the manifest. It has to be present as an expose in the built bundle."
137
+ );
138
+ var contributionId = z.string().regex(CONTRIBUTION_ID).describe(
139
+ "The id for this contribution, and the handle the host and your generated types refer to it by."
140
+ );
141
+ var title = z.string().min(1).max(60).describe("The label owners read on this contribution.");
142
+ var scopeEntry = z.object({
143
+ key: z.enum(SCOPE_KEYS, {
144
+ error: "Unknown scope key; see the Scopes reference."
145
+ }).describe("The scope key, from the scopes reference."),
146
+ justification: z.string().min(20).max(500).describe(
147
+ "Why this extension asks for the scope, in your own words. Shown on the consent screen, and required for the scopes that ask for one."
148
+ ).optional()
149
+ }).strict();
150
+ var mobile = z.boolean().describe(
151
+ "Whether this surface is drawn on phone layouts. Defaults to true; set false to hide it there."
152
+ ).optional();
153
+ var hostPage = (what) => z.enum(PAGE_KEYS).describe(what);
154
+ var pageContribution = z.object({
155
+ id: contributionId,
156
+ title,
157
+ entry: entryPath,
158
+ nav: z.boolean().describe(
159
+ "Whether this page also gets an entry in the project navigation. The route `/projects/:projectId/ext/:extensionId/:pageId` exists either way."
160
+ ).optional(),
161
+ mobile
162
+ }).strict();
163
+ var tabContribution = z.object({
164
+ id: contributionId,
165
+ title,
166
+ entry: entryPath,
167
+ page: hostPage("The host page this tab sits on."),
168
+ mobile
169
+ }).strict();
170
+ var widgetContribution = z.object({
171
+ id: contributionId,
172
+ title,
173
+ entry: entryPath,
174
+ page: hostPage("The host page this widget renders on."),
175
+ slot: z.enum(SLOTS).describe(
176
+ "The named anchor on that page. A slot that is not on the page fails validation."
177
+ ),
178
+ size: z.enum(["1x1", "2x1", "2x2"]).describe(
179
+ "The widget's footprint in the slot grid, as columns by rows. Every slot takes all three."
180
+ ),
181
+ expandable: z.boolean().describe(
182
+ "Whether owners can open the same component in the expand panel."
183
+ ).optional()
184
+ }).strict();
185
+ var seriesContribution = z.object({
186
+ id: contributionId,
187
+ label: title,
188
+ chart: z.enum(CHARTS).describe("The host chart this line is drawn on."),
189
+ metric: z.string().min(1).max(60).describe(
190
+ "The connector metric this line plots. It has to be one the connector declares."
191
+ )
192
+ }).strict();
193
+ var bannerContribution = z.object({
194
+ id: contributionId,
195
+ page: hostPage("The host page this banner appears on."),
196
+ event: z.string().regex(DOMAIN_EVENT).describe(
197
+ 'The event that raises it. Declare the same name under `contributes.webhooks` with `as: "banner"`.'
198
+ )
199
+ }).strict();
200
+ var rowActionContribution = z.object({
201
+ id: contributionId,
202
+ title,
203
+ table: z.enum(TABLE_KEYS).describe("The host table whose row menu gains this entry."),
204
+ entry: entryPath
205
+ }).strict();
206
+ var markerContribution = z.object({
207
+ id: contributionId,
208
+ label: title,
209
+ chart: z.union([z.enum(CHARTS), z.literal("*")]).describe(
210
+ "The host chart this marker is drawn on, or `*` for every chart."
211
+ ),
212
+ event: z.string().regex(DOMAIN_EVENT).describe(
213
+ 'The event that places it on the time axis. Declare the same name under `contributes.webhooks` with `as: "event"`.'
214
+ ),
215
+ glyph: z.string().min(1).max(2).describe("The one or two characters drawn at the marker.")
216
+ }).strict();
217
+ var settingsContribution = z.object({
218
+ entry: entryPath,
219
+ schema: jsonSchema.describe(
220
+ 'The JSON Schema for the values owners fill in. It becomes `TapSettingsProps["values"]` and the reserved `settings` singleton.'
221
+ )
222
+ }).strict();
223
+ var storageCollection = z.object({
224
+ scope: z.enum(["user", "install", "organization"]).describe(
225
+ "Who the documents belong to: one `user`, the whole `install`, or the `organization`."
226
+ ),
227
+ singleton: z.boolean().describe(
228
+ "Whether the collection holds exactly one document rather than many."
229
+ ).optional(),
230
+ schema: jsonSchema.describe(
231
+ "The JSON Schema for one document in this collection."
232
+ )
233
+ }).strict();
234
+ var credentialField = z.object({
235
+ name: z.string().regex(SNAKE_CASE).describe("The key your server reads this value back as."),
236
+ label: z.string().min(1).max(60).describe("The label owners read above the input."),
237
+ type: z.enum(["text", "password", "textarea", "file", "select"]).describe("How the input is drawn to the owner."),
238
+ required: z.boolean().describe("Whether owners have to fill it in before the install works."),
239
+ options: z.array(z.string()).describe("The choices offered, for a `select` field.").optional(),
240
+ help: z.string().max(200).describe("The hint drawn under the input.").optional()
241
+ }).strict();
242
+ var connectorContribution = z.object({
243
+ auth: z.object({
244
+ method: z.enum(["none", "api_key", "oauth"]).describe("The credential flow owners go through at install."),
245
+ fields: z.array(credentialField).max(20).describe(
246
+ "The credentials owners enter. They reach your server as `credentials` on every call."
247
+ ).optional(),
248
+ oauth: z.object({
249
+ authorizationUrl: httpsUrl.describe(
250
+ "Where owners are sent to approve access."
251
+ ),
252
+ tokenUrl: httpsUrl.describe(
253
+ "Where the authorization code is exchanged for a token."
254
+ ),
255
+ scopes: z.array(z.string()).describe(
256
+ "The scopes asked of the outside service. These are that service's scopes, not Tappify's."
257
+ )
258
+ }).strict().describe('The endpoints, for `method: "oauth"`.').optional()
259
+ }).strict().describe("How owners authenticate this connector.").optional(),
260
+ sync: z.enum(["5m", "15m", "1h", "6h", "24h"]).describe("How often Tappify asks your server to sync.").optional(),
261
+ metrics: z.array(
262
+ z.object({
263
+ key: z.string().regex(SNAKE_CASE).describe("The metric key. A series names it to draw its line."),
264
+ label: z.string().min(1).max(60).describe("The label owners read on the metric."),
265
+ unit: z.enum(["count", "ratio", "currency", "seconds", "bytes"]).describe("How the value is formatted."),
266
+ kind: z.enum(["gauge", "counter"]).describe(
267
+ "Whether the value is a level at a point in time (`gauge`) or accumulates (`counter`)."
268
+ ),
269
+ dimensions: z.array(
270
+ z.object({
271
+ key: z.string().regex(SNAKE_CASE).describe("The dimension key."),
272
+ label: z.string().min(1).max(60).describe("The label owners read on the dimension.")
273
+ }).strict()
274
+ ).max(10).describe("The breakdowns this metric can be split by.").optional(),
275
+ backfillDays: z.number().int().min(1).max(90).describe("How many days of history to fetch on the first sync.").optional()
276
+ }).strict()
277
+ ).max(50).describe(
278
+ "The metrics this connector writes into the metrics store. Needs the `metrics:write` scope."
279
+ ).optional()
280
+ }).strict();
281
+ var webhookContribution = z.object({
282
+ event: z.string().regex(DOMAIN_EVENT).describe("The event name your server posts, as `domain.thing`."),
283
+ as: z.enum(["alert", "banner", "event"]).describe(
284
+ "What Tappify turns it into: an inbox `alert`, a page `banner`, or an `event` a chart marker and `tap.data.subscribe` can read."
285
+ ),
286
+ payload: jsonSchema.describe(
287
+ "The JSON Schema for the body your server posts. An alert or a banner has to carry `why`."
288
+ )
289
+ }).strict();
290
+ var aiContribution = z.object({
291
+ tools: z.array(
292
+ z.object({
293
+ id: contributionId,
294
+ description: z.string().min(10).max(500).describe(
295
+ "What this tool does, in the words the assistant reads when it decides whether to call it."
296
+ ),
297
+ input: aiInputSchema.describe(
298
+ "The JSON Schema for the arguments the assistant passes. The assistant reads the `title` and `description` strings inside it, so they are scanned at publish and the whole schema is bounded at 16 KB."
299
+ ),
300
+ returns: z.enum(["comparison", "table", "series", "list", "value"]).describe("The card the host draws from the result."),
301
+ cost: z.enum(["low", "medium", "high"]).describe(
302
+ "Roughly what one call costs to serve, so the assistant can prefer a cheaper tool."
303
+ ),
304
+ cache: z.enum(["1m", "5m", "1h"]).describe(
305
+ "How long an identical call may be served from cache instead of reaching your server."
306
+ ).optional()
307
+ }).strict()
308
+ ).max(15, "An extension declares at most 15 assistant tools.").describe(
309
+ "The tools the assistant may call on your server. Needs the `ai:tools` scope."
310
+ ).optional(),
311
+ mentions: z.array(
312
+ z.object({
313
+ id: contributionId,
314
+ label: title,
315
+ list: z.string().min(1).max(200).describe(
316
+ "The path that answers the picker with `{ items: [{ id, label }] }`. The host always calls `/tappify/mentions/<id>` under `server.baseUrl` \u2014 the route `createTappifyHandler` serves \u2014 so write that path here; it records the route, it does not choose it."
317
+ )
318
+ }).strict()
319
+ ).max(10).describe(
320
+ "The things owners can name with @ in chat, and where to list them. Needs the `ai:tools` scope."
321
+ ).optional(),
322
+ actions: z.array(
323
+ z.object({
324
+ id: contributionId,
325
+ description: z.string().min(10).max(500).describe(
326
+ "What this action does, in the words the owner will read on the approval card."
327
+ ),
328
+ input: aiInputSchema.describe(
329
+ "The JSON Schema for the arguments the assistant passes. The assistant reads the `title` and `description` strings inside it, so they are scanned at publish and the whole schema is bounded at 16 KB."
330
+ ),
331
+ approval: z.literal(true).describe(
332
+ "Always true. An action goes through an owner approval; there is no way to opt out."
333
+ ),
334
+ reversible: z.boolean().describe("Whether the owner can undo it after it runs."),
335
+ scope: z.enum(SCOPE_KEYS).describe(
336
+ "The scope this action needs. Declare it under `scopes` as well."
337
+ ),
338
+ estimatesCost: z.boolean().describe(
339
+ "Whether the approval card will show what the action costs."
340
+ ).optional()
341
+ }).strict()
342
+ ).max(10, "An extension declares at most 10 assistant actions.").describe(
343
+ "The actions the assistant may run, each behind an owner approval. Needs the `ai:actions` scope, and each action's own scope. `tap.actions.run` creates the run and resolves with it; the host draws the approval card and calls your server only once the owner approves."
344
+ ).optional(),
345
+ knowledge: z.array(
346
+ z.object({
347
+ id: contributionId,
348
+ category: z.enum(["metric_definitions", "gotchas", "setup"]).describe("What kind of knowledge the file carries."),
349
+ file: z.string().min(1).max(200).describe(
350
+ "The markdown file in the bundle, as a path relative to the manifest."
351
+ )
352
+ }).strict()
353
+ ).max(20).describe(
354
+ "Markdown the assistant indexes and cites when it answers about this extension."
355
+ ).optional(),
356
+ skills: z.array(
357
+ z.object({
358
+ id: contributionId,
359
+ name: z.string().min(1).max(60).describe("The skill name the assistant matches on."),
360
+ description: z.string().min(10).max(300).describe("When the assistant should reach for this skill."),
361
+ file: z.string().min(1).max(200).describe(
362
+ "The markdown file in the bundle, as a path relative to the manifest."
363
+ ),
364
+ tools: z.array(z.string()).max(15).describe("The tool ids this skill is allowed to use.").optional()
365
+ }).strict()
366
+ ).max(5, "An extension declares at most 5 skills.").describe(
367
+ "Markdown procedures that shape how the assistant works. Needs the `ai:skills` scope."
368
+ ).optional(),
369
+ prompts: z.array(
370
+ z.object({
371
+ id: contributionId,
372
+ title,
373
+ template: z.string().min(1).max(2e3).describe(
374
+ "The prompt text, with `{{project.*}}`, `{{context.*}}` and `{{input.*}}` placeholders. The owner sees it resolved before it is sent."
375
+ ),
376
+ surfaces: z.array(
377
+ z.enum(["chat_suggestion", "widget", "page", "result_card"])
378
+ ).min(1).describe("Where the host draws this prompt as a chip."),
379
+ after: z.array(z.string()).max(15).describe(
380
+ "The tool ids whose results this prompt follows up on. They have to be this extension's own."
381
+ ).optional(),
382
+ input: jsonSchema.describe(
383
+ "The JSON Schema for a short form the owner fills in before the prompt is sent."
384
+ ).optional()
385
+ }).strict()
386
+ ).max(20, "An extension declares at most 20 prompt templates.").describe(
387
+ "Prompt templates the host offers owners as chips. Needs the `ai:skills` scope."
388
+ ).optional(),
389
+ context: z.array(
390
+ z.object({
391
+ id: contributionId,
392
+ description: z.string().min(10).max(300).describe("What context this provider adds to the turn."),
393
+ when: z.array(z.enum(["turn_start", "mention"])).min(1).describe("When the host calls your server for it."),
394
+ cache: z.enum(["1m", "5m", "1h"]).describe("How long the context may be reused before refetching.").optional()
395
+ }).strict()
396
+ ).max(3, "An extension declares at most 3 context providers.").describe(
397
+ "Providers the host calls to put your data in front of the assistant. Needs the `ai:skills` scope."
398
+ ).optional()
399
+ }).strict();
400
+ var statusNames = (stage) => z.array(z.string()).describe(`Your own status names that mean ${stage}.`);
401
+ var workContribution = z.object({
402
+ kind: z.enum(["tracker", "docs", "chat"]).describe("What kind of destination this is."),
403
+ container: z.object({
404
+ label: z.string().min(1).max(60).describe(
405
+ 'What you call a container, such as "Project", "Space" or "Channel". Owners read it when they pick one.'
406
+ ),
407
+ list: z.string().min(1).describe(
408
+ "The path under `server.baseUrl` that lists the containers an owner can choose from."
409
+ )
410
+ }).strict().describe("Where items land, and how owners pick one."),
411
+ creates: z.array(z.enum(["issue", "page", "message"])).min(1).describe("What this destination can create."),
412
+ fields: z.object({
413
+ priority: z.array(z.string()).describe("Your own priority names, highest first.").optional(),
414
+ labels: z.boolean().describe("Whether items carry labels.").optional(),
415
+ assignee: z.boolean().describe("Whether items carry an assignee.").optional(),
416
+ required: z.array(z.string()).describe(
417
+ "The fields an owner has to fill in before you can create."
418
+ )
419
+ }).strict().describe("What a created item carries."),
420
+ attachments: z.array(z.enum(["image", "link"])).describe("What can be attached to a created item."),
421
+ comments: z.enum(["two-way", "none"]).describe("Whether comments sync back and forth or not at all."),
422
+ status: z.object({
423
+ webhook: z.string().regex(DOMAIN_EVENT).describe("The event your server posts when an item changes status."),
424
+ map: z.object({
425
+ open: statusNames("not started"),
426
+ in_progress: statusNames("under way"),
427
+ done: statusNames("finished")
428
+ }).strict().describe(
429
+ "Your status names, grouped into the three stages Tappify draws."
430
+ )
431
+ }).strict().describe("How status travels back from your system.").optional(),
432
+ identity: z.enum(["user", "install"]).describe(
433
+ "Whether items are created as the owner who asked (`user`) or as the install itself."
434
+ )
435
+ }).strict();
436
+ var listing = z.object({
437
+ longDescription: z.string().min(1).max(5e3).describe("The body of your listing page, in markdown.").optional(),
438
+ screenshots: z.array(z.string().min(1)).min(1, "A listing shows 1 to 6 screenshots.").max(6, "A listing shows 1 to 6 screenshots.").describe(
439
+ "Screenshot files carried in the bundle, as paths relative to the manifest. png at 1600 by 1000."
440
+ ).optional(),
441
+ website: httpsUrl.describe("Where owners go to learn more about this extension.").optional(),
442
+ supportUrl: httpsUrl.describe("Where owners go when something breaks.").optional(),
443
+ privacyUrl: httpsUrl.describe(
444
+ "Your privacy policy, linked from the consent screen before anyone installs."
445
+ ).optional()
446
+ }).strict();
447
+ var LISTING_FIELDS = [
448
+ "longDescription",
449
+ "screenshots",
450
+ "website",
451
+ "supportUrl",
452
+ "privacyUrl"
453
+ ];
454
+ var procedureDeclaration = z.object({
455
+ input: jsonSchema.describe(
456
+ "The JSON Schema for the argument your handler receives. The host validates against it before calling."
457
+ ),
458
+ output: jsonSchema.describe(
459
+ "The JSON Schema for what your handler returns."
460
+ ),
461
+ cache: z.enum(["1m", "5m", "1h"]).describe(
462
+ "How long an identical call may be served from cache instead of reaching your server."
463
+ ).optional(),
464
+ kind: z.enum(["read", "write"]).describe("Whether this procedure only reads or also changes something.").optional()
465
+ }).strict();
466
+ var manifestObject = z.object({
467
+ $schema: z.string().describe(
468
+ "The URL of this JSON Schema. Editors read it for completion and inline validation."
469
+ ).optional(),
470
+ id: z.string().regex(SLUG).describe(
471
+ "The identifier for this extension, unique across the Tap Store and fixed after the first publish."
472
+ ),
473
+ name: z.string().min(2).max(40).describe(
474
+ "The name owners see in the Tap Store and on every surface this extension renders."
475
+ ),
476
+ description: z.string().min(10).max(200).describe(
477
+ "The one line owners read on the Tap Store card and on the install screen."
478
+ ),
479
+ icon: z.string().min(1).max(200).describe(
480
+ "The icon file carried in the bundle, as a path relative to the manifest. Square png or svg, at most 512 KB."
481
+ ),
482
+ category: z.enum(CATEGORIES, {
483
+ error: "Choose a category from the Tap Store list (see the Categories reference)."
484
+ }).describe("The Tap Store section owners browse and filter by."),
485
+ sdk: z.string().min(1).max(60).describe(
486
+ 'The SDK range this build targets, such as "^2.0.0". The host serves one major at a time and rejects a range that excludes it.'
487
+ ),
488
+ visibility: z.enum(["public", "unlisted", "private"]).describe(
489
+ 'Who can find and install this extension. "public" lists it in the Tap Store and needs all five listing fields; "unlisted" installs by link; "private" installs only in your own workspace.'
490
+ ),
491
+ installScope: z.enum(["project", "organization"]).describe(
492
+ "Whether one install belongs to a single project or to a whole organization. An organization install contributes no pages, tabs, widgets or series, because those render on a project."
493
+ ),
494
+ scopes: z.array(scopeEntry).min(1).max(SCOPE_KEYS.length).describe(
495
+ "The permissions an owner grants at install. Declare only what your contributions use."
496
+ ),
497
+ contributes: z.object({
498
+ pages: z.array(pageContribution).max(20).describe(
499
+ "Pages of this extension's own, each on its own route under the project. `nav: true` also puts one in the project navigation."
500
+ ).optional(),
501
+ tabs: z.array(tabContribution).max(20).describe("Tabs beside the host's own on a named host page.").optional(),
502
+ widgets: z.array(widgetContribution).max(20).describe("Cards drawn into a named slot on a host page.").optional(),
503
+ series: z.array(seriesContribution).max(20).describe(
504
+ "Lines drawn onto a host chart from a metric the connector writes. The host draws the line on the chart this names and puts the vendor in the chart legend; a stale metric is drawn dashed with the time of its last sync."
505
+ ).optional(),
506
+ banners: z.array(bannerContribution).max(20).describe(
507
+ 'Banners raised on a host page by a webhook declared with as: "banner". The host draws one at the top of that page, carrying the `why` sentence from the delivery, until the owner dismisses it.'
508
+ ).optional(),
509
+ rowActions: z.array(rowActionContribution).max(20).describe("Actions offered on a row of a host table.").optional(),
510
+ markers: z.array(markerContribution).max(20).describe(
511
+ 'Markers drawn on a host chart by a webhook declared with as: "event". Needs the `ui:render` and `alerts:write` scopes. The host draws the glyph on the chart time axis at the moment the delivery arrived, with the vendor named on hover.'
512
+ ).optional(),
513
+ settings: settingsContribution.describe("The settings panel owners open for this install.").optional(),
514
+ storage: z.record(z.string().regex(SNAKE_CASE), storageCollection).describe(
515
+ 'The hosted storage collections this extension reads and writes. At most 10, and the name "settings" is reserved. Needs the `storage:write` scope.'
516
+ ).optional(),
517
+ connector: connectorContribution.describe(
518
+ "How this extension authenticates against an outside service, how often it syncs, and the metrics it writes."
519
+ ).optional(),
520
+ webhooks: z.array(webhookContribution).max(20).describe(
521
+ "The events this extension's server posts to Tappify, and what each one becomes. An `alert` or a `banner` needs the `alerts:write` scope."
522
+ ).optional(),
523
+ ai: aiContribution.describe(
524
+ "What this extension offers the assistant: tools, mentions, actions, knowledge, skills, prompts and context providers."
525
+ ).optional(),
526
+ work: workContribution.describe(
527
+ "The work destination this extension creates issues, pages or messages in. Needs the `work:create` and `work:sync` scopes."
528
+ ).optional()
529
+ }).strict().describe(
530
+ "What this extension adds to the host. At most 20 UI contributions in total, and a page, tab, widget, row action, marker or settings panel needs the `ui:render` scope."
531
+ ),
532
+ server: z.object({
533
+ baseUrl: httpsUrl.describe(
534
+ "Where the host calls your server. `<baseUrl>/tappify/health` answers 200 within three seconds at publish."
535
+ ),
536
+ sandboxBaseUrl: httpsUrl.describe(
537
+ "Where dev installs and sandbox projects call instead. Defaults to `baseUrl`."
538
+ ).optional(),
539
+ procedures: z.record(
540
+ z.string().regex(
541
+ PROCEDURE_NAME,
542
+ 'Procedure names are identifiers such as "getSummary" or "get-summary": letters, digits, _ and -, starting with a letter, at most 60 characters. They become URL segments and generated type names.'
543
+ ),
544
+ procedureDeclaration
545
+ ).describe(
546
+ "The procedures the host may call on your server, by name, each with its input and output schema. At most 30."
547
+ ).optional(),
548
+ events: z.array(
549
+ z.enum(EVENTS, {
550
+ error: (issue) => `"${String(issue.input)}" is not a Tappify event. Subscribe to a name from the event catalogue.`
551
+ })
552
+ ).describe(
553
+ "The Tappify events your server subscribes to. Each is delivered to `POST /tappify/events` as it happens, with the same body every host-to-vendor call carries."
554
+ ).optional()
555
+ }).strict().describe(
556
+ "Your own server: where the host calls it, and what it answers. Required once you declare a connector, a webhook, an assistant contribution, a work destination or a procedure."
557
+ ).optional(),
558
+ telemetry: z.array(
559
+ z.string().regex(
560
+ SNAKE_CASE,
561
+ 'Telemetry names are snake_case, such as "funnel_viewed".'
562
+ ).max(60)
563
+ ).max(20).describe(
564
+ "The telemetry names this extension may report. `tap.telemetry.event` rejects a name that is not declared here."
565
+ ).optional(),
566
+ listing: listing.describe(
567
+ "What owners read on your Tap Store listing page. A public extension needs all five fields."
568
+ ).optional(),
569
+ pricing: z.never().describe(
570
+ "Reserved. Tappify sets pricing outside the manifest, so a manifest that carries this field fails validation."
571
+ ).optional(),
572
+ score: z.never().describe(
573
+ "Reserved. Tappify computes the quality score, so a manifest that carries this field fails validation."
574
+ ).optional()
575
+ }).strict();
576
+ function sdkMajors(range) {
577
+ const majors = /* @__PURE__ */ new Set();
578
+ for (const alternative of range.split("||")) {
579
+ for (const token of alternative.trim().split(/\s+/)) {
580
+ if (token.startsWith("<")) continue;
581
+ const match = /^(?:\^|~|>=|>|=)?\s*(\d+)/.exec(token);
582
+ if (match) majors.add(Number(match[1]));
583
+ }
584
+ }
585
+ return [...majors].sort((left, right) => left - right);
586
+ }
587
+ function uiCount(contributes) {
588
+ return (contributes.pages?.length ?? 0) + (contributes.tabs?.length ?? 0) + (contributes.widgets?.length ?? 0) + (contributes.series?.length ?? 0) + (contributes.banners?.length ?? 0) + (contributes.rowActions?.length ?? 0) + (contributes.markers?.length ?? 0) + (contributes.settings ? 1 : 0);
589
+ }
590
+ function hasRenderedSurface(contributes) {
591
+ return (contributes.pages?.length ?? 0) > 0 || (contributes.tabs?.length ?? 0) > 0 || (contributes.widgets?.length ?? 0) > 0 || (contributes.rowActions?.length ?? 0) > 0 || (contributes.markers?.length ?? 0) > 0 || contributes.settings !== void 0;
592
+ }
593
+ var manifestSchema = manifestObject.superRefine((manifest, ctx) => {
594
+ const granted = new Set(manifest.scopes.map((scope) => scope.key));
595
+ const contributes = manifest.contributes;
596
+ const requireScope = (key, path, because) => {
597
+ if (granted.has(key)) return;
598
+ ctx.addIssue({
599
+ code: "custom",
600
+ path,
601
+ message: `Add the ${key} scope: ${because}`
602
+ });
603
+ };
604
+ for (const [index, widget] of (contributes.widgets ?? []).entries()) {
605
+ const slots = slotsForPage(widget.page);
606
+ if (!slots.includes(widget.slot)) {
607
+ ctx.addIssue({
608
+ code: "custom",
609
+ path: ["contributes", "widgets", index, "slot"],
610
+ message: `Slot "${widget.slot}" is not on the ${widget.page} page. Pick one of: ${slots.join(", ")}.`
611
+ });
612
+ }
613
+ }
614
+ if (hasRenderedSurface(contributes)) {
615
+ requireScope(
616
+ "ui:render",
617
+ ["scopes"],
618
+ "this extension renders UI in the host."
619
+ );
620
+ }
621
+ if (uiCount(contributes) > 20) {
622
+ ctx.addIssue({
623
+ code: "custom",
624
+ path: ["contributes"],
625
+ message: "An extension declares at most 20 UI contributions. Remove some, or split the extension in two."
626
+ });
627
+ }
628
+ if (contributes.storage) {
629
+ requireScope(
630
+ "storage:write",
631
+ ["scopes"],
632
+ "this extension declares hosted storage collections."
633
+ );
634
+ if (Object.keys(contributes.storage).length > 10) {
635
+ ctx.addIssue({
636
+ code: "custom",
637
+ path: ["contributes", "storage"],
638
+ message: "An extension declares at most 10 storage collections."
639
+ });
640
+ }
641
+ if ("settings" in contributes.storage) {
642
+ ctx.addIssue({
643
+ code: "custom",
644
+ path: ["contributes", "storage", "settings"],
645
+ message: 'The collection name "settings" is reserved for the settings contribution. Rename this collection.'
646
+ });
647
+ }
648
+ }
649
+ if (contributes.connector?.metrics?.length) {
650
+ requireScope(
651
+ "metrics:write",
652
+ ["scopes"],
653
+ "the connector writes metrics into the metrics store."
654
+ );
655
+ }
656
+ const webhookEventsAs = (as) => new Set(
657
+ (contributes.webhooks ?? []).filter((webhook) => webhook.as === as).map((webhook) => webhook.event)
658
+ );
659
+ const bannerEvents = webhookEventsAs("banner");
660
+ const markerEvents = webhookEventsAs("event");
661
+ if ((contributes.webhooks ?? []).some(
662
+ (webhook) => webhook.as === "alert" || webhook.as === "banner"
663
+ )) {
664
+ requireScope(
665
+ "alerts:write",
666
+ ["scopes"],
667
+ "a declared webhook becomes an alert or a banner."
668
+ );
669
+ }
670
+ for (const [index, banner] of (contributes.banners ?? []).entries()) {
671
+ if (!bannerEvents.has(banner.event)) {
672
+ ctx.addIssue({
673
+ code: "custom",
674
+ path: ["contributes", "banners", index, "event"],
675
+ message: `Event "${banner.event}" is not declared under webhooks. Declare it with as: "banner".`
676
+ });
677
+ }
678
+ }
679
+ for (const [index, marker] of (contributes.markers ?? []).entries()) {
680
+ if (!markerEvents.has(marker.event)) {
681
+ ctx.addIssue({
682
+ code: "custom",
683
+ path: ["contributes", "markers", index, "event"],
684
+ message: `Event "${marker.event}" is not declared under webhooks. Declare it with as: "event".`
685
+ });
686
+ }
687
+ }
688
+ if ((contributes.markers?.length ?? 0) > 0) {
689
+ requireScope("ui:render", ["scopes"], "markers draw on a host chart.");
690
+ requireScope(
691
+ "alerts:write",
692
+ ["scopes"],
693
+ "markers come from declared webhooks."
694
+ );
695
+ }
696
+ const metricKeys = new Set(
697
+ (contributes.connector?.metrics ?? []).map((metric) => metric.key)
698
+ );
699
+ for (const [index, series] of (contributes.series ?? []).entries()) {
700
+ if (!metricKeys.has(series.metric)) {
701
+ ctx.addIssue({
702
+ code: "custom",
703
+ path: ["contributes", "series", index, "metric"],
704
+ message: `Metric "${series.metric}" is not declared by the connector. Add it under contributes.connector.metrics.`
705
+ });
706
+ }
707
+ }
708
+ const ai = contributes.ai;
709
+ if (ai?.tools?.length || ai?.mentions?.length) {
710
+ requireScope("ai:tools", ["scopes"], "the assistant calls declared tools.");
711
+ }
712
+ if (ai?.actions?.length) {
713
+ requireScope(
714
+ "ai:actions",
715
+ ["scopes"],
716
+ "declared actions run behind approval."
717
+ );
718
+ for (const [index, action] of ai.actions.entries()) {
719
+ if (!granted.has(action.scope)) {
720
+ ctx.addIssue({
721
+ code: "custom",
722
+ path: ["contributes", "ai", "actions", index, "scope"],
723
+ message: `Add the ${action.scope} scope: the action "${action.id}" declares it.`
724
+ });
725
+ }
726
+ }
727
+ }
728
+ if (ai?.skills?.length || ai?.prompts?.length || ai?.context?.length) {
729
+ requireScope(
730
+ "ai:skills",
731
+ ["scopes"],
732
+ "skills, prompts and context providers shape the assistant."
733
+ );
734
+ }
735
+ const toolIds = new Set((ai?.tools ?? []).map((tool) => tool.id));
736
+ for (const [index, prompt] of (ai?.prompts ?? []).entries()) {
737
+ for (const [afterIndex, toolId] of (prompt.after ?? []).entries()) {
738
+ if (!toolIds.has(toolId)) {
739
+ ctx.addIssue({
740
+ code: "custom",
741
+ path: ["contributes", "ai", "prompts", index, "after", afterIndex],
742
+ message: `Tool "${toolId}" is not declared by this extension. Use one of your own tool ids.`
743
+ });
744
+ }
745
+ }
746
+ }
747
+ if (contributes.work) {
748
+ requireScope(
749
+ "work:create",
750
+ ["scopes"],
751
+ "the work destination creates items."
752
+ );
753
+ requireScope(
754
+ "work:sync",
755
+ ["scopes"],
756
+ "the work destination syncs comments and status."
757
+ );
758
+ }
759
+ const needsServer = contributes.connector !== void 0 || (contributes.webhooks?.length ?? 0) > 0 || ai !== void 0 || contributes.work !== void 0 || manifest.server?.procedures !== void 0;
760
+ if (needsServer && !manifest.server?.baseUrl) {
761
+ ctx.addIssue({
762
+ code: "custom",
763
+ path: ["server", "baseUrl"],
764
+ message: "Set server.baseUrl: connectors, webhooks, assistant contributions, work destinations and procedures all call your server."
765
+ });
766
+ }
767
+ const procedures = manifest.server?.procedures ?? {};
768
+ if (Object.keys(procedures).length > 30) {
769
+ ctx.addIssue({
770
+ code: "custom",
771
+ path: ["server", "procedures"],
772
+ message: "An extension declares at most 30 procedures."
773
+ });
774
+ }
775
+ for (const scope of manifest.scopes) {
776
+ const definition = findScope(scope.key);
777
+ if (definition?.requiresJustification && !scope.justification) {
778
+ ctx.addIssue({
779
+ code: "custom",
780
+ path: ["scopes"],
781
+ message: `${scope.key} needs a justification of 20 to 500 characters that tells the owner why you ask for it.`
782
+ });
783
+ }
784
+ }
785
+ if (manifest.visibility === "public") {
786
+ const missing = LISTING_FIELDS.filter(
787
+ (field) => manifest.listing?.[field] === void 0
788
+ );
789
+ if (missing.length > 0) {
790
+ ctx.addIssue({
791
+ code: "custom",
792
+ path: ["listing"],
793
+ message: `A public extension needs a complete listing: add ${missing.join(", ")}. Set them with \`tappify extension set listing.<field> <value>\`, or keep visibility "unlisted" or "private" until the listing is ready.`
794
+ });
795
+ }
796
+ }
797
+ if (manifest.installScope === "organization") {
798
+ const forbidden = ["pages", "tabs", "widgets", "series"].filter(
799
+ (key) => (contributes[key]?.length ?? 0) > 0
800
+ );
801
+ if (forbidden.length > 0) {
802
+ ctx.addIssue({
803
+ code: "custom",
804
+ path: ["installScope"],
805
+ message: `Organization-scoped extensions cannot contribute widgets, tabs, pages or series, because those render on a project. Remove ${forbidden.join(", ")}, or set installScope to "project".`
806
+ });
807
+ }
808
+ }
809
+ if (!sdkMajors(manifest.sdk).some((major) => SERVED_SDK_MAJORS.includes(major))) {
810
+ ctx.addIssue({
811
+ code: "custom",
812
+ path: ["sdk"],
813
+ message: `The host serves SDK major ${SERVED_SDK_MAJORS.join(", ")}. Set sdk to a range that includes it, such as "^2.0.0".`
814
+ });
815
+ }
816
+ });
817
+ function manifestJsonSchema() {
818
+ const { $schema, ...generated } = z.toJSONSchema(manifestObject, {
819
+ target: "draft-2020-12"
820
+ });
821
+ return {
822
+ $schema,
823
+ $id: "https://schema.tappify.ai/extension/v2.json",
824
+ title: "Tappify extension manifest",
825
+ description: "The tappify.extension.json file at the root of an extension repository.",
826
+ ...generated
827
+ };
828
+ }
829
+ function issueMessage(issue) {
830
+ if (issue.code !== "invalid_key") return issue.message;
831
+ return issue.issues[0]?.message ?? issue.message;
832
+ }
833
+ function validateManifest(value) {
834
+ const result = manifestSchema.safeParse(value);
835
+ if (result.success) {
836
+ return { ok: true, manifest: result.data };
837
+ }
838
+ const issues = result.error.issues.map((issue) => ({
839
+ path: issue.path.length > 0 ? issue.path.join(".") : "(root)",
840
+ message: issueMessage(issue)
841
+ }));
842
+ return { ok: false, issues };
843
+ }
844
+ export {
845
+ CARD_KEYS,
846
+ CATEGORIES,
847
+ CHARTS,
848
+ EVENTS,
849
+ KINDS,
850
+ PAGES,
851
+ PAGE_KEYS,
852
+ QUERY_SCOPES,
853
+ SCOPES,
854
+ SCOPE_KEYS,
855
+ SERVED_SDK_MAJORS,
856
+ SLOTS,
857
+ TABLES,
858
+ TABLE_KEYS,
859
+ defineManifest,
860
+ exposeName,
861
+ findPage,
862
+ findScope,
863
+ findTable,
864
+ manifestJsonSchema,
865
+ manifestSchema,
866
+ remoteName,
867
+ sdkMajors,
868
+ slotsForPage,
869
+ uiContributions,
870
+ validateManifest
871
+ };
872
+ //# sourceMappingURL=manifest.mjs.map