@tappify/extension-sdk 2.1.0-beta.2 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (137) hide show
  1. package/README.md +18 -54
  2. package/dist/{chunk-6HETNZJY.cjs → chunk-2DBSVKAU.cjs} +1 -1
  3. package/dist/{chunk-6HETNZJY.cjs.map → chunk-2DBSVKAU.cjs.map} +1 -1
  4. package/dist/{chunk-O357OJXL.cjs → chunk-3YWVYA42.cjs} +18 -14
  5. package/dist/chunk-3YWVYA42.cjs.map +1 -0
  6. package/dist/{chunk-GAUZYBXE.cjs → chunk-5AAPD5WV.cjs} +11 -11
  7. package/dist/chunk-5AAPD5WV.cjs.map +1 -0
  8. package/dist/{chunk-SGEYDV3K.mjs → chunk-BDCX6LRQ.mjs} +3 -3
  9. package/dist/chunk-BDCX6LRQ.mjs.map +1 -0
  10. package/dist/{chunk-KZ5NGFUP.mjs → chunk-C3YNVHBP.mjs} +7 -20
  11. package/dist/chunk-C3YNVHBP.mjs.map +1 -0
  12. package/dist/{chunk-QXD7WVKK.mjs → chunk-CSTYBKZC.mjs} +1 -1
  13. package/dist/chunk-CSTYBKZC.mjs.map +1 -0
  14. package/dist/{chunk-SWG7CNTZ.mjs → chunk-CTSZ5D7S.mjs} +5 -9
  15. package/dist/chunk-CTSZ5D7S.mjs.map +1 -0
  16. package/dist/chunk-EW54H7CR.cjs +722 -0
  17. package/dist/chunk-EW54H7CR.cjs.map +1 -0
  18. package/dist/chunk-EX3NFXDK.mjs +722 -0
  19. package/dist/chunk-EX3NFXDK.mjs.map +1 -0
  20. package/dist/{chunk-A74HCO6T.cjs → chunk-G7XZSN2Q.cjs} +18 -31
  21. package/dist/chunk-G7XZSN2Q.cjs.map +1 -0
  22. package/dist/{chunk-RBIQT75L.mjs → chunk-GG3IX52W.mjs} +2 -2
  23. package/dist/{chunk-SLGOHIBL.mjs → chunk-IGK2AP6J.mjs} +17 -13
  24. package/dist/chunk-IGK2AP6J.mjs.map +1 -0
  25. package/dist/{chunk-VV7ELNRW.cjs → chunk-JMUK3K47.cjs} +3 -3
  26. package/dist/{chunk-VV7ELNRW.cjs.map → chunk-JMUK3K47.cjs.map} +1 -1
  27. package/dist/{chunk-SGCYEEXD.mjs → chunk-MNDVYPEP.mjs} +2 -9
  28. package/dist/chunk-MNDVYPEP.mjs.map +1 -0
  29. package/dist/{chunk-BPCGXXMD.cjs → chunk-NFUPPCX4.cjs} +18 -22
  30. package/dist/chunk-NFUPPCX4.cjs.map +1 -0
  31. package/dist/{chunk-PRCZMOLD.cjs → chunk-OPPYEA7F.cjs} +3 -3
  32. package/dist/{chunk-PRCZMOLD.cjs.map → chunk-OPPYEA7F.cjs.map} +1 -1
  33. package/dist/{chunk-7YKH4LZN.cjs → chunk-PYJMJF44.cjs} +2 -9
  34. package/dist/chunk-PYJMJF44.cjs.map +1 -0
  35. package/dist/{chunk-QA5AL3VZ.mjs → chunk-QHM7XNRK.mjs} +2 -2
  36. package/dist/{chunk-QA5AL3VZ.mjs.map → chunk-QHM7XNRK.mjs.map} +1 -1
  37. package/dist/codegen.cjs.map +1 -1
  38. package/dist/codegen.d.cts +7 -43
  39. package/dist/codegen.d.ts +7 -43
  40. package/dist/codegen.mjs.map +1 -1
  41. package/dist/{constants-Bh759FUI.d.cts → constants-DJ_KgZpN.d.cts} +8 -44
  42. package/dist/{constants-Bh759FUI.d.ts → constants-DJ_KgZpN.d.ts} +8 -44
  43. package/dist/{context-mr8R5CNt.d.ts → context-Dt6xh_5t.d.cts} +4 -5
  44. package/dist/{context-DI0EJCRD.d.cts → context-Ml7k_V14.d.ts} +4 -5
  45. package/dist/{errors-Cqh0wrqt.d.ts → errors-COsBh7kd.d.cts} +2 -3
  46. package/dist/{errors-Cqh0wrqt.d.cts → errors-COsBh7kd.d.ts} +2 -3
  47. package/dist/handler-CZIlZ5vu.d.ts +227 -0
  48. package/dist/handler-kU9Jf36K.d.cts +227 -0
  49. package/dist/host.cjs +4 -4
  50. package/dist/host.d.cts +4 -4
  51. package/dist/host.d.ts +4 -4
  52. package/dist/host.mjs +3 -3
  53. package/dist/index.cjs +34 -5
  54. package/dist/index.cjs.map +1 -1
  55. package/dist/index.d.cts +627 -69
  56. package/dist/index.d.ts +627 -69
  57. package/dist/index.mjs +33 -4
  58. package/dist/manifest.cjs +24 -136
  59. package/dist/manifest.cjs.map +1 -1
  60. package/dist/manifest.d.cts +12 -97
  61. package/dist/manifest.d.ts +12 -97
  62. package/dist/manifest.mjs +10 -122
  63. package/dist/manifest.mjs.map +1 -1
  64. package/dist/server.cjs +66 -14
  65. package/dist/server.cjs.map +1 -1
  66. package/dist/server.d.cts +6 -6
  67. package/dist/server.d.ts +6 -6
  68. package/dist/server.mjs +61 -9
  69. package/dist/server.mjs.map +1 -1
  70. package/dist/styles.css +367 -0
  71. package/dist/testing/index.cjs +13 -72
  72. package/dist/testing/index.cjs.map +1 -1
  73. package/dist/testing/index.d.cts +10 -29
  74. package/dist/testing/index.d.ts +10 -29
  75. package/dist/testing/index.mjs +13 -72
  76. package/dist/testing/index.mjs.map +1 -1
  77. package/dist/testing/jest.cjs +5 -5
  78. package/dist/testing/jest.mjs +2 -2
  79. package/dist/testing/vitest.cjs +5 -5
  80. package/dist/testing/vitest.cjs.map +1 -1
  81. package/dist/testing/vitest.d.cts +0 -10
  82. package/dist/testing/vitest.d.ts +0 -10
  83. package/dist/testing/vitest.mjs +2 -2
  84. package/dist/testing/vitest.mjs.map +1 -1
  85. package/dist/{types-uGHrRaOj.d.ts → types-DDAGLpov.d.ts} +3 -4
  86. package/dist/{types-BdDaLnqs.d.cts → types-DrpQSv_2.d.cts} +3 -4
  87. package/dist/vite-preview.cjs +125 -294
  88. package/dist/vite-preview.cjs.map +1 -1
  89. package/dist/vite-preview.d.cts +24 -6
  90. package/dist/vite-preview.d.ts +24 -6
  91. package/dist/vite-preview.mjs +118 -287
  92. package/dist/vite-preview.mjs.map +1 -1
  93. package/dist/vite.cjs +24 -137
  94. package/dist/vite.cjs.map +1 -1
  95. package/dist/vite.d.cts +7 -43
  96. package/dist/vite.d.ts +7 -43
  97. package/dist/vite.mjs +18 -131
  98. package/dist/vite.mjs.map +1 -1
  99. package/package.json +2 -12
  100. package/schema/extension-v2.json +121 -440
  101. package/dist/chunk-2CY5HVY7.mjs +0 -59
  102. package/dist/chunk-2CY5HVY7.mjs.map +0 -1
  103. package/dist/chunk-7YKH4LZN.cjs.map +0 -1
  104. package/dist/chunk-A74HCO6T.cjs.map +0 -1
  105. package/dist/chunk-BPCGXXMD.cjs.map +0 -1
  106. package/dist/chunk-EB5XWQAU.cjs +0 -132
  107. package/dist/chunk-EB5XWQAU.cjs.map +0 -1
  108. package/dist/chunk-GAUZYBXE.cjs.map +0 -1
  109. package/dist/chunk-KZ5NGFUP.mjs.map +0 -1
  110. package/dist/chunk-O357OJXL.cjs.map +0 -1
  111. package/dist/chunk-ONJMPO3P.cjs +0 -59
  112. package/dist/chunk-ONJMPO3P.cjs.map +0 -1
  113. package/dist/chunk-QJMZRV7A.mjs +0 -132
  114. package/dist/chunk-QJMZRV7A.mjs.map +0 -1
  115. package/dist/chunk-QXD7WVKK.mjs.map +0 -1
  116. package/dist/chunk-SGCYEEXD.mjs.map +0 -1
  117. package/dist/chunk-SGEYDV3K.mjs.map +0 -1
  118. package/dist/chunk-SLGOHIBL.mjs.map +0 -1
  119. package/dist/chunk-SWG7CNTZ.mjs.map +0 -1
  120. package/dist/client-BO3kavn0.d.ts +0 -25
  121. package/dist/client-DE-vMgdO.d.cts +0 -25
  122. package/dist/ui/styles.css +0 -740
  123. package/dist/ui.cjs +0 -1018
  124. package/dist/ui.cjs.map +0 -1
  125. package/dist/ui.d.cts +0 -231
  126. package/dist/ui.d.ts +0 -231
  127. package/dist/ui.mjs +0 -1018
  128. package/dist/ui.mjs.map +0 -1
  129. package/dist/vite-preview-bootstrap.cjs +0 -66
  130. package/dist/vite-preview-bootstrap.cjs.map +0 -1
  131. package/dist/vite-preview-bootstrap.d.cts +0 -15
  132. package/dist/vite-preview-bootstrap.d.ts +0 -15
  133. package/dist/vite-preview-bootstrap.mjs +0 -66
  134. package/dist/vite-preview-bootstrap.mjs.map +0 -1
  135. package/dist/work-events-BlOVpK0k.d.ts +0 -547
  136. package/dist/work-events-D97EoqvS.d.cts +0 -547
  137. /package/dist/{chunk-RBIQT75L.mjs.map → chunk-GG3IX52W.mjs.map} +0 -0
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts","../src/client/ui/cn.ts","../src/client/ui/primitives.tsx","../src/client/ui/TapDialog.tsx","../src/client/ui/TapForm.tsx"],"sourcesContent":["export { 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';\nexport * from './client/ui';\n","import clsx, { type ClassValue as ClsxClassValue } from 'clsx';\n\n/**\n * Anything `cn` accepts: a class string, a number, an array of these, a map of\n * class name to condition, or a value to drop.\n */\nexport type ClassValue = ClsxClassValue;\n\n/**\n * Joins class names, dropping the ones that are `false`, `null` or `undefined`.\n *\n * @remarks\n * This is `clsx`, re-exported so a vendor surface needs no class-name dependency\n * of its own. It does not merge conflicting utility classes.\n *\n * @example\n * ```tsx\n * import { cn } from '@tappify/extension-sdk';\n *\n * function Row({ compact }: { compact: boolean }) {\n * return <div className={cn('starter-row', compact && 'starter-row--compact')} />;\n * }\n * ```\n */\nexport function cn(...values: ClassValue[]): string {\n return clsx(values);\n}\n","import {\n useId,\n type ButtonHTMLAttributes,\n type InputHTMLAttributes,\n type ReactElement,\n type ReactNode,\n type SelectHTMLAttributes,\n type TextareaHTMLAttributes,\n} from 'react';\nimport { cn } from './cn';\n\n/** The props `TapButton` takes, on top of the native button attributes. */\nexport interface TapButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {\n /** Defaults to `primary`. */\n variant?: 'primary' | 'secondary' | 'ghost' | 'danger';\n /** Defaults to `md`. */\n size?: 'sm' | 'md';\n /** Disables the button and marks it `aria-busy`. */\n loading?: boolean;\n}\n\n/**\n * Renders a button in the host's styling, in one of four variants.\n *\n * @remarks\n * Draws on `--tap-accent` and `--tap-accent-fg` for `primary`, `--bg-raised`,\n * `--fg-default` and `--divider` for `secondary`, `--fg-muted` for `ghost` and\n * `--tap-danger` for `danger`, and takes its radius from `--tap-radius`. The\n * element defaults to `type=\"button\"`, so a button inside a `TapForm` does not\n * submit it unless you pass `type=\"submit\"`. `loading` disables the button, so a\n * click cannot land twice.\n *\n * @example\n * ```tsx\n * import { TapButton } from '@tappify/extension-sdk';\n *\n * function ExportRow({ busy, onExport }: { busy: boolean; onExport: () => void }) {\n * return (\n * <TapButton variant=\"secondary\" size=\"sm\" loading={busy} onClick={onExport}>\n * Export\n * </TapButton>\n * );\n * }\n * ```\n */\nexport function TapButton({\n variant = 'primary',\n size = 'md',\n loading = false,\n disabled,\n className,\n children,\n ...rest\n}: TapButtonProps): ReactElement {\n return (\n <button\n type=\"button\"\n {...rest}\n disabled={disabled === true || loading}\n aria-busy={loading || undefined}\n className={cn(\n 'tap-btn',\n `tap-btn--${variant}`,\n size === 'sm' && 'tap-btn--sm',\n className,\n )}\n >\n {children}\n </button>\n );\n}\n\n/** The props `TapCard` takes. */\nexport interface TapCardProps {\n /** Rendered in the header, which appears only with a `title` or an `action`. */\n title?: ReactNode;\n /** Sits at the end of the header, for a button or a link. */\n action?: ReactNode;\n /** Rendered under a divider below the body. */\n footer?: ReactNode;\n className?: string;\n children: ReactNode;\n}\n\n/**\n * Renders the bordered surface a widget, tab or page section sits in.\n *\n * @remarks\n * Renders as a `section` on `--bg-raised` with a `--divider` border and the\n * `--tap-radius` corner, and the footer in `--fg-muted`. The header appears only\n * when `title` or `action` is given, and the body carries its own padding, so\n * nest content directly rather than wrapping it in another padded box. A widget\n * belongs in one card.\n *\n * @example\n * ```tsx\n * import { TapButton, TapCard, TapStat } from '@tappify/extension-sdk';\n *\n * function InstallSummary() {\n * return (\n * <TapCard\n * title=\"Install summary\"\n * action={<TapButton variant=\"ghost\" size=\"sm\">Compact</TapButton>}\n * footer={<span>Last seven days</span>}\n * >\n * <TapStat label=\"Installs\" value=\"1,260\" delta={0.087} />\n * </TapCard>\n * );\n * }\n * ```\n */\nexport function TapCard({\n title,\n action,\n footer,\n className,\n children,\n}: TapCardProps): ReactElement {\n return (\n <section className={cn('tap-card', className)}>\n {(title !== undefined || action !== undefined) && (\n <header className=\"tap-card__head\">\n <span>{title}</span>\n {action}\n </header>\n )}\n <div className=\"tap-card__body\">{children}</div>\n {footer !== undefined && (\n <footer className=\"tap-card__foot\">{footer}</footer>\n )}\n </section>\n );\n}\n\n/** The props `TapPageHeader` takes. */\nexport interface TapPageHeaderProps {\n title: ReactNode;\n description?: ReactNode;\n /** Laid out in a row at the end of the header. */\n actions?: ReactNode;\n}\n\n/**\n * Renders the title, description and actions at the top of a tab, a page or a\n * settings panel.\n *\n * @remarks\n * The title renders as an `h2`, because the host owns the page's `h1`. The\n * description follows `--fg-muted` and the rule under the header follows\n * `--divider`. Under 360 pixels of mount width the actions stack under the title.\n * A widget in a slot uses `TapCard`'s own header instead.\n *\n * @example\n * ```tsx\n * import { TapButton, TapPageHeader } from '@tappify/extension-sdk';\n *\n * function FunnelsHeader() {\n * return (\n * <TapPageHeader\n * title=\"Funnels\"\n * description=\"Every step from impression to install.\"\n * actions={<TapButton size=\"sm\">New funnel</TapButton>}\n * />\n * );\n * }\n * ```\n */\nexport function TapPageHeader({\n title,\n description,\n actions,\n}: TapPageHeaderProps): ReactElement {\n return (\n <div className=\"tap-page-header\">\n <div>\n <h2 className=\"tap-page-header__title\">{title}</h2>\n {description !== undefined && (\n <p className=\"tap-page-header__description\">{description}</p>\n )}\n </div>\n {actions !== undefined && <div className=\"tap-row\">{actions}</div>}\n </div>\n );\n}\n\n/** The props `TapSkeleton` takes. */\nexport interface TapSkeletonProps {\n /** How many bars to draw. Defaults to one. */\n lines?: number;\n /** A CSS width for every bar. Left out, each bar is 12% shorter than the last. */\n width?: string;\n className?: string;\n}\n\n/**\n * Renders the placeholder bars that stand in for content while a read is in\n * flight.\n *\n * @remarks\n * The bars are `aria-hidden`, so a screen reader is not told about them, and they\n * take their tone from `--bg-subtle`. The pulse stops under\n * `prefers-reduced-motion`. Render this while `isLoading` is `true`, in place of\n * the content rather than beside it.\n *\n * @example\n * ```tsx\n * import { TapSkeleton, TapStat, useTapQuery } from '@tappify/extension-sdk';\n *\n * function Keywords() {\n * const keywords = useTapQuery({ kind: 'keywords' });\n * if (keywords.isLoading) return <TapSkeleton lines={3} />;\n * return <TapStat label=\"Tracked\" value={keywords.data?.keywords.length ?? 0} />;\n * }\n * ```\n */\nexport function TapSkeleton({\n lines = 1,\n width,\n className,\n}: TapSkeletonProps): ReactElement {\n return (\n <div aria-hidden=\"true\">\n {Array.from({ length: lines }, (_, index) => (\n <span\n key={index}\n className={cn('tap-skeleton', className)}\n style={{ width: width ?? `${String(100 - index * 12)}%` }}\n />\n ))}\n </div>\n );\n}\n\n/** The props `TapEmptyState` takes. */\nexport interface TapEmptyStateProps {\n title: string;\n description?: string;\n /** Rendered under the text, for the one thing the owner can do next. */\n action?: ReactNode;\n}\n\n/**\n * Renders the centred message that stands in for content when there is nothing to\n * show.\n *\n * @remarks\n * Centres its text in `--fg-muted` with the title in `--fg-default`. Use it for an\n * empty result or a surface the install has not been granted; a failure belongs in\n * `TapErrorState`, which carries a retry and announces itself as an alert.\n *\n * @example\n * ```tsx\n * import { TapEmptyState, useTapQuery } from '@tappify/extension-sdk';\n *\n * function Reviews() {\n * const reviews = useTapQuery({\n * kind: 'reviews',\n * range: { from: '2026-09-01', to: '2026-09-08' },\n * });\n * if (reviews.data?.total === 0) {\n * return <TapEmptyState title=\"No reviews in this range\" />;\n * }\n * return <span>{reviews.data?.total ?? 0} reviews</span>;\n * }\n * ```\n */\nexport function TapEmptyState({\n title,\n description,\n action,\n}: TapEmptyStateProps): ReactElement {\n return (\n <div className=\"tap-state\">\n <span className=\"tap-state__title\">{title}</span>\n {description !== undefined && <span>{description}</span>}\n {action}\n </div>\n );\n}\n\n/** The props `TapErrorState` takes. */\nexport interface TapErrorStateProps {\n title: string;\n /** The failure's own message, which is what a caller shows the owner. */\n description?: string;\n /** An error code, rendered small in `--font-mono` under the description. */\n code?: string;\n /** Renders a Retry button, and is called by it. */\n onRetry?: () => void;\n}\n\n/**\n * Renders the centred failure message a surface shows in place of its content.\n *\n * @remarks\n * Carries `role=\"alert\"`, so the host announces it, and draws the title in\n * `--tap-danger`. The Retry button appears only with `onRetry`; wire it to the\n * `refetch` of every read the surface needs. Pass the failure's own `message` as\n * the description — an error from `useTapServer` carries the text your server sent\n * the owner — and the `code` of a `TapError` or `TapServerError` as `code`.\n *\n * @example\n * ```tsx\n * import { TapErrorState, useTapQuery } from '@tappify/extension-sdk';\n *\n * function Crashes() {\n * const crashes = useTapQuery({\n * kind: 'crashes',\n * range: { from: '2026-09-01', to: '2026-09-08' },\n * });\n * if (crashes.error !== null) {\n * return (\n * <TapErrorState\n * title=\"Crashes did not load\"\n * description={crashes.error.message}\n * onRetry={crashes.refetch}\n * />\n * );\n * }\n * return <span>{crashes.data?.issues.length ?? 0} issues</span>;\n * }\n * ```\n */\nexport function TapErrorState({\n title,\n description,\n code,\n onRetry,\n}: TapErrorStateProps): ReactElement {\n return (\n <div className=\"tap-state tap-state--error\" role=\"alert\">\n <span className=\"tap-state__title\">{title}</span>\n {description !== undefined && <span>{description}</span>}\n {code !== undefined && <span className=\"tap-state__code\">{code}</span>}\n {onRetry !== undefined && (\n <TapButton variant=\"secondary\" size=\"sm\" onClick={onRetry}>\n Retry\n </TapButton>\n )}\n </div>\n );\n}\n\n/** The props `TapStat` takes. */\nexport interface TapStatProps {\n label: string;\n value: ReactNode;\n /** A share of the previous period, so `0.087` renders as `+9%`. */\n delta?: number;\n /** A line of context under the number. */\n hint?: string;\n}\n\n/**\n * Renders one labelled number, with an optional change against the previous\n * period.\n *\n * @remarks\n * The label is uppercased in `--fg-muted` and the number uses tabular figures, so\n * a row of stats stays aligned as values change. `delta` is a ratio, not a\n * percentage: it renders as whole percent with a plus in front when it is above\n * zero, and is coloured `--tap-danger` below zero and `--tap-accent` at or above\n * it. Format `value`\n * yourself with `tap.format.number` or `tap.format.currency`, so it follows the\n * owner's locale.\n *\n * @example\n * ```tsx\n * import { TapStat, useTap } from '@tappify/extension-sdk';\n *\n * function Installs({ installs, delta }: { installs: number; delta: number }) {\n * const tap = useTap();\n * return (\n * <TapStat\n * label=\"Installs\"\n * value={tap.format.number(installs)}\n * delta={delta}\n * hint=\"Against the previous seven days\"\n * />\n * );\n * }\n * ```\n */\nexport function TapStat({\n label,\n value,\n delta,\n hint,\n}: TapStatProps): ReactElement {\n const percent =\n delta === undefined\n ? undefined\n : `${delta > 0 ? '+' : ''}${String(Math.round(delta * 100))}%`;\n\n return (\n <div className=\"tap-stat\">\n <span className=\"tap-stat__label\">{label}</span>\n <span className=\"tap-stat__value\">{value}</span>\n {percent !== undefined && (\n <span\n className={cn(\n delta !== undefined && delta < 0\n ? 'tap-stat__delta--down'\n : 'tap-stat__delta--up',\n )}\n >\n {percent}\n </span>\n )}\n {hint !== undefined && <span className=\"tap-field__hint\">{hint}</span>}\n </div>\n );\n}\n\ninterface FieldFrameProps {\n id: string;\n label?: string;\n hint?: string;\n error?: string;\n hintId: string;\n errorId: string;\n children: ReactNode;\n}\n\nfunction FieldFrame({\n id,\n label,\n hint,\n error,\n hintId,\n errorId,\n children,\n}: FieldFrameProps): ReactElement {\n return (\n <div className=\"tap-field\">\n {label !== undefined && (\n <label className=\"tap-field__label\" htmlFor={id}>\n {label}\n </label>\n )}\n {children}\n {hint !== undefined && (\n <span className=\"tap-field__hint\" id={hintId}>\n {hint}\n </span>\n )}\n {error !== undefined && (\n <span className=\"tap-field__error\" id={errorId}>\n {error}\n </span>\n )}\n </div>\n );\n}\n\nfunction describedByOf(\n fieldId: string,\n hint: string | undefined,\n error: string | undefined,\n): string | undefined {\n const value = cn(\n hint !== undefined && `${fieldId}-hint`,\n error !== undefined && `${fieldId}-error`,\n );\n return value === '' ? undefined : value;\n}\n\n/**\n * The `TapInput` props for a single-line control, on top of the native input\n * attributes minus `size`.\n */\nexport interface TapSingleLineInputProps extends Omit<\n InputHTMLAttributes<HTMLInputElement>,\n 'size'\n> {\n /** Rendered as a `label` bound to the control by id. */\n label?: string;\n /** Marks the control invalid and renders the message under it. */\n error?: string;\n /** A line of guidance under the control, referenced by `aria-describedby`. */\n hint?: string;\n}\n\nfunction TapSingleLineInput({\n label,\n error,\n hint,\n className,\n id,\n ...rest\n}: TapSingleLineInputProps): ReactElement {\n const generated = useId();\n const fieldId = id ?? generated;\n\n return (\n <FieldFrame\n id={fieldId}\n label={label}\n hint={hint}\n error={error}\n hintId={`${fieldId}-hint`}\n errorId={`${fieldId}-error`}\n >\n <input\n {...rest}\n id={fieldId}\n className={cn(\n 'tap-control',\n error !== undefined && 'tap-control--invalid',\n className,\n )}\n aria-invalid={error !== undefined || undefined}\n aria-describedby={describedByOf(fieldId, hint, error)}\n />\n </FieldFrame>\n );\n}\n\n/**\n * The `TapInput` props for a multiline control, on top of the native textarea\n * attributes.\n */\nexport interface TapTextareaProps extends TextareaHTMLAttributes<HTMLTextAreaElement> {\n /** Rendered as a `label` bound to the control by id. */\n label?: string;\n /** Marks the control invalid and renders the message under it. */\n error?: string;\n /** A line of guidance under the control, referenced by `aria-describedby`. */\n hint?: string;\n}\n\nfunction TapTextarea({\n label,\n error,\n hint,\n className,\n id,\n rows,\n ...rest\n}: TapTextareaProps): ReactElement {\n const generated = useId();\n const fieldId = id ?? generated;\n\n return (\n <FieldFrame\n id={fieldId}\n label={label}\n hint={hint}\n error={error}\n hintId={`${fieldId}-hint`}\n errorId={`${fieldId}-error`}\n >\n <textarea\n {...rest}\n id={fieldId}\n rows={rows ?? 4}\n className={cn(\n 'tap-control',\n error !== undefined && 'tap-control--invalid',\n className,\n )}\n aria-invalid={error !== undefined || undefined}\n aria-describedby={describedByOf(fieldId, hint, error)}\n />\n </FieldFrame>\n );\n}\n\n/**\n * The props `TapInput` takes: the single-line set, or the textarea set behind\n * `multiline`.\n */\nexport type TapInputProps =\n | ({ multiline?: false } & TapSingleLineInputProps)\n | ({ multiline: true } & TapTextareaProps);\n\n/**\n * Renders a labelled text control with its hint and error, as an input or, with\n * `multiline`, a textarea.\n *\n * @remarks\n * Generates an id when none is passed and binds the label, the hint and the error\n * to the control, so the field is announced whole; an `error` also sets\n * `aria-invalid` and the `--tap-danger` border. The textarea starts at four rows\n * and is resizable in height. Pass `type=\"checkbox\"` with\n * `className=\"tap-control--checkbox\"` for a boolean, which is what `TapForm`\n * does.\n *\n * @example\n * ```tsx\n * import { TapInput } from '@tappify/extension-sdk';\n *\n * function TermField({ term, onTerm }: { term: string; onTerm: (next: string) => void }) {\n * return (\n * <TapInput\n * label=\"Keyword\"\n * hint=\"One term per row in the report.\"\n * value={term}\n * onChange={event => onTerm(event.target.value)}\n * />\n * );\n * }\n * ```\n */\nexport function TapInput(props: TapInputProps): ReactElement {\n if (props.multiline === true) {\n const { multiline: _multiline, ...rest } = props;\n return <TapTextarea {...rest} />;\n }\n\n const { multiline: _multiline, ...rest } = props;\n return <TapSingleLineInput {...rest} />;\n}\n\n/** The props `TapSelect` takes, on top of the native select attributes. */\nexport interface TapSelectProps extends SelectHTMLAttributes<HTMLSelectElement> {\n /** Rendered as a `label` bound to the control by id. */\n label?: string;\n /** Marks the control invalid and renders the message under it. */\n error?: string;\n /** A line of guidance under the control, referenced by `aria-describedby`. */\n hint?: string;\n /** Rendered in order; the component adds no empty option of its own. */\n options: { value: string; label: string }[];\n}\n\n/**\n * Renders a labelled select over a list of options, with its hint and error.\n *\n * @remarks\n * Carries the same label, hint, error and `aria-describedby` wiring as\n * `TapInput`. Add your own empty option when the field starts unset — `TapForm`\n * puts a `Choose one` option in front of a schema's enum values.\n *\n * @example\n * ```tsx\n * import { TapSelect } from '@tappify/extension-sdk';\n *\n * function PlatformPicker({ value, onPick }: { value: string; onPick: (next: string) => void }) {\n * return (\n * <TapSelect\n * label=\"Platform\"\n * value={value}\n * options={[\n * { value: 'ios', label: 'iOS' },\n * { value: 'android', label: 'Android' },\n * ]}\n * onChange={event => onPick(event.target.value)}\n * />\n * );\n * }\n * ```\n */\nexport function TapSelect({\n label,\n error,\n hint,\n options,\n className,\n id,\n ...rest\n}: TapSelectProps): ReactElement {\n const generated = useId();\n const fieldId = id ?? generated;\n\n return (\n <FieldFrame\n id={fieldId}\n label={label}\n hint={hint}\n error={error}\n hintId={`${fieldId}-hint`}\n errorId={`${fieldId}-error`}\n >\n <select\n {...rest}\n id={fieldId}\n className={cn(\n 'tap-control',\n error !== undefined && 'tap-control--invalid',\n className,\n )}\n aria-invalid={error !== undefined || undefined}\n aria-describedby={describedByOf(fieldId, hint, error)}\n >\n {options.map(option => (\n <option key={option.value} value={option.value}>\n {option.label}\n </option>\n ))}\n </select>\n </FieldFrame>\n );\n}\n\n/** One column of a `TapTable`. */\nexport interface TapTableColumnDef<T> {\n /** The key read from each row when there is no `render`. */\n key: string;\n label: string;\n /** `end` right-aligns the column and gives it tabular figures. */\n align?: 'start' | 'end';\n /** Draws the cell. Without it the cell shows the row's own string or number. */\n render?(row: T): ReactNode;\n}\n\n/** The props `TapTable` takes. */\nexport interface TapTableProps<T> {\n columns: TapTableColumnDef<T>[];\n rows: T[];\n /** Returns a stable key per row, which React uses for reconciliation. */\n rowKey: (row: T) => string;\n /** Rendered in place of the whole table when `rows` is empty. */\n empty?: ReactNode;\n /** Makes each row clickable and focusable, and is called on click or Enter. */\n onRowClick?: (row: T) => void;\n}\n\nfunction cellText(row: unknown, key: string): string {\n if (row === null || typeof row !== 'object') return '';\n const value: unknown = Reflect.get(row, key);\n if (typeof value === 'string') return value;\n if (\n typeof value === 'number' ||\n typeof value === 'boolean' ||\n typeof value === 'bigint'\n ) {\n return String(value);\n }\n return '';\n}\n\n/**\n * Renders rows under column headers, scrolling sideways inside the mount rather\n * than widening it.\n *\n * @remarks\n * A cell with no `render` shows the row's own value under `key` when that value is\n * a string, number, boolean or bigint, and nothing otherwise, so an object or an\n * array needs a `render`. With `empty` and no rows, the table is replaced whole by\n * that node. With `onRowClick` each row becomes focusable and answers Enter and\n * Space as well as a click, and hovers on `--bg-subtle`; the headers and rules\n * follow `--fg-muted` and `--divider`.\n *\n * @example\n * ```tsx\n * import { TapEmptyState, TapTable, useTapQuery } from '@tappify/extension-sdk';\n *\n * function Keywords() {\n * const keywords = useTapQuery({ kind: 'keywords' });\n * return (\n * <TapTable\n * columns={[\n * { key: 'term', label: 'Term' },\n * { key: 'position', label: 'Rank', align: 'end' },\n * ]}\n * rows={keywords.data?.keywords ?? []}\n * rowKey={keyword => keyword.id}\n * empty={<TapEmptyState title=\"No keywords tracked yet\" />}\n * />\n * );\n * }\n * ```\n */\nexport function TapTable<T>({\n columns,\n rows,\n rowKey,\n empty,\n onRowClick,\n}: TapTableProps<T>): ReactElement {\n if (rows.length === 0 && empty !== undefined) {\n return <>{empty}</>;\n }\n\n return (\n <div className=\"tap-table-wrap\">\n <table className=\"tap-table\">\n <thead>\n <tr>\n {columns.map(column => (\n <th\n key={column.key}\n className={cn(column.align === 'end' && 'tap-table__end')}\n scope=\"col\"\n >\n {column.label}\n </th>\n ))}\n </tr>\n </thead>\n <tbody>\n {rows.map(row => (\n <tr\n key={rowKey(row)}\n data-clickable={onRowClick !== undefined}\n tabIndex={onRowClick === undefined ? undefined : 0}\n onClick={\n onRowClick === undefined ? undefined : () => onRowClick(row)\n }\n onKeyDown={\n onRowClick === undefined\n ? undefined\n : event => {\n if (event.key !== 'Enter' && event.key !== ' ') return;\n event.preventDefault();\n onRowClick(row);\n }\n }\n >\n {columns.map(column => (\n <td\n key={column.key}\n className={cn(column.align === 'end' && 'tap-table__end')}\n >\n {column.render\n ? column.render(row)\n : cellText(row, column.key)}\n </td>\n ))}\n </tr>\n ))}\n </tbody>\n </table>\n </div>\n );\n}\n","import {\n useCallback,\n useEffect,\n useId,\n useRef,\n type KeyboardEvent,\n type ReactElement,\n type ReactNode,\n} from 'react';\nimport { createPortal } from 'react-dom';\nimport { useTap } from '../context';\nimport { TapError } from '../errors';\n\nconst DIALOG_CLOSE_EVENT = 'tap:dialog-close';\n\nconst FOCUSABLE =\n 'a[href], button:not([disabled]), textarea:not([disabled]), input:not([disabled]), select:not([disabled]), [tabindex]:not([tabindex=\"-1\"])';\n\n/**\n * `document.activeElement` is the mount's shadow host whenever focus sits\n * inside the shadow tree, so the trap resolves focus from the dialog's own root.\n */\nfunction activeElementNear(node: Node | null): Element | null {\n const root = node?.getRootNode();\n return root instanceof ShadowRoot\n ? root.activeElement\n : document.activeElement;\n}\n\n/** The props `TapDialog` takes. */\nexport interface TapDialogProps {\n /** The dialog renders nothing while this is `false`. */\n open: boolean;\n /** Rendered in the header, and used as the dialog's accessible name. */\n title: string;\n /** Called by Escape, by a click on the scrim, and by the host's close event. */\n onClose: () => void;\n /** Rendered under a divider at the end, for the dialog's buttons. */\n footer?: ReactNode;\n children: ReactNode;\n /** The id of your own heading, when the accessible name is not the title. */\n labelledBy?: string;\n}\n\n/**\n * Renders a modal dialog into the mount's own portal node, so it covers the\n * surface without leaving the extension.\n *\n * @remarks\n * Rendered outside a Tappify mount it throws `TAP_OUTSIDE_HOST`, from the `useTap`\n * above it. Inside a mount whose shadow root has no portal node yet, opening it\n * throws `TapError` with code `TAP_NO_PORTAL`; `renderWithTap` provides that node\n * in tests. While open it traps Tab\n * inside itself, moves focus to the first focusable element and returns focus\n * where it was on close, and answers Escape and a click on the scrim with\n * `onClose`. The scrim follows `--tap-scrim` and the panel `--bg-raised`,\n * `--divider` and `--tap-radius`; the panel is at most 520 pixels wide. The\n * component keeps no open state of its own.\n *\n * @example\n * ```tsx\n * import { TapButton, TapDialog } from '@tappify/extension-sdk';\n * import { useState } from 'react';\n *\n * function ExportDialog() {\n * const [open, setOpen] = useState(false);\n * return (\n * <>\n * <TapButton onClick={() => setOpen(true)}>Export</TapButton>\n * <TapDialog\n * open={open}\n * title=\"Export keywords\"\n * onClose={() => setOpen(false)}\n * footer={<TapButton onClick={() => setOpen(false)}>Done</TapButton>}\n * >\n * <span>The report covers the range in the filter bar.</span>\n * </TapDialog>\n * </>\n * );\n * }\n * ```\n */\nexport function TapDialog({\n open,\n title,\n onClose,\n footer,\n children,\n labelledBy,\n}: TapDialogProps): ReactElement | null {\n const tap = useTap();\n const dialogRef = useRef<HTMLDivElement | null>(null);\n const titleId = useId();\n\n const focusables = useCallback((): HTMLElement[] => {\n const root = dialogRef.current;\n if (root === null) return [];\n return Array.from(root.querySelectorAll<HTMLElement>(FOCUSABLE));\n }, []);\n\n useEffect(() => {\n if (!open) return;\n\n const previous = activeElementNear(dialogRef.current);\n focusables()[0]?.focus();\n\n return () => {\n if (previous instanceof HTMLElement) previous.focus();\n };\n }, [open, focusables]);\n\n useEffect(() => {\n const portal = tap.__portal;\n if (!open || portal === null) return;\n\n const close = (): void => {\n onClose();\n };\n\n portal.addEventListener(DIALOG_CLOSE_EVENT, close);\n return () => {\n portal.removeEventListener(DIALOG_CLOSE_EVENT, close);\n };\n }, [open, tap, onClose]);\n\n if (!open) return null;\n\n if (tap.__portal === null) {\n throw new TapError(\n 'TAP_NO_PORTAL',\n 'TapDialog needs the host portal node, which the mount sets inside its shadow root. Render this dialog from a contribution entry, or use renderWithTap() in tests.',\n );\n }\n\n const onKeyDown = (event: KeyboardEvent<HTMLDivElement>): void => {\n if (event.key === 'Escape') {\n event.stopPropagation();\n event.preventDefault();\n onClose();\n return;\n }\n\n if (event.key !== 'Tab') return;\n\n const items = focusables();\n if (items.length === 0) return;\n\n const first = items[0];\n const last = items[items.length - 1];\n const active = activeElementNear(dialogRef.current);\n\n if (event.shiftKey && active === first) {\n event.preventDefault();\n last.focus();\n return;\n }\n\n if (!event.shiftKey && active === last) {\n event.preventDefault();\n first.focus();\n }\n };\n\n return createPortal(\n <div\n className=\"tap-dialog-scrim\"\n onKeyDown={onKeyDown}\n onMouseDown={event => {\n if (event.target === event.currentTarget) onClose();\n }}\n >\n <div\n ref={dialogRef}\n className=\"tap-dialog\"\n role=\"dialog\"\n aria-modal=\"true\"\n aria-labelledby={labelledBy ?? titleId}\n >\n <header className=\"tap-dialog__head\" id={titleId}>\n {title}\n </header>\n <div className=\"tap-dialog__body\">{children}</div>\n {footer !== undefined && (\n <footer className=\"tap-dialog__foot\">{footer}</footer>\n )}\n </div>\n </div>,\n tap.__portal,\n );\n}\n","import {\n useMemo,\n useState,\n type ReactElement,\n type SubmitEventHandler,\n} from 'react';\nimport { z } from 'zod';\nimport type { JsonSchema } from '../../manifest/types';\nimport { TapButton, TapInput, TapSelect } from './primitives';\n\n/** The control a schema property maps to. */\nexport type TapFormFieldKind =\n | 'text'\n | 'textarea'\n | 'date'\n | 'number'\n | 'boolean'\n | 'enum';\n\n/** One control `TapForm` renders, as `formFields` reads it off a schema. */\nexport interface TapFormField {\n /** The property name in the schema, and the key in the submitted value. */\n name: string;\n /** The property's `title`, or the name spaced and capitalised. */\n label: string;\n kind: TapFormFieldKind;\n /** Whether the schema lists the property under `required`. */\n required: boolean;\n /** The property's `description`. */\n hint?: string;\n /** Set for an `enum` property: one option per string value. */\n options?: { value: string; label: string }[];\n}\n\nfunction humanise(name: string): string {\n const spaced = name\n .replace(/[_-]+/g, ' ')\n .replace(/([a-z0-9])([A-Z])/g, '$1 $2')\n .trim();\n return spaced.charAt(0).toUpperCase() + spaced.slice(1);\n}\n\nfunction readString(\n source: Record<string, unknown>,\n key: string,\n): string | undefined {\n const value = source[key];\n return typeof value === 'string' ? value : undefined;\n}\n\nfunction kindOf(property: Record<string, unknown>): TapFormFieldKind {\n if (Array.isArray(property.enum)) return 'enum';\n\n const type = readString(property, 'type');\n if (type === 'boolean') return 'boolean';\n if (type === 'number' || type === 'integer') return 'number';\n\n const format = readString(property, 'format');\n if (format === 'textarea') return 'textarea';\n if (format === 'date') return 'date';\n return 'text';\n}\n\n/**\n * Reads a JSON Schema object and returns the list of controls `TapForm` renders\n * from it.\n *\n * @remarks\n * One field per declared property, in the order the schema lists them; a schema\n * with no `properties` object returns an empty list, and a property that is not an\n * object is skipped. An `enum` becomes a select and keeps only its string values;\n * otherwise the kind comes from `type` — `boolean`, `number` and `integer` — and\n * then from `format`, where `textarea` and `date` are recognised and anything else\n * falls back to a single-line text control. Nested objects and arrays get a text\n * control, so render those yourself. Call it when you want the fields without the\n * form.\n *\n * @example\n * ```ts\n * import { formFields } from '@tappify/extension-sdk';\n *\n * const fields = formFields({\n * type: 'object',\n * properties: { refreshMinutes: { type: 'integer', title: 'Refresh minutes' } },\n * required: ['refreshMinutes'],\n * });\n * const names = fields.map(field => field.name);\n * ```\n */\nexport function formFields(schema: JsonSchema): TapFormField[] {\n const properties = schema.properties;\n if (typeof properties !== 'object' || properties === null) return [];\n\n const required = new Set(\n Array.isArray(schema.required)\n ? schema.required.filter(\n (name): name is string => typeof name === 'string',\n )\n : [],\n );\n\n return Object.entries(properties).flatMap(([name, raw]) => {\n if (typeof raw !== 'object' || raw === null) return [];\n const property: Record<string, unknown> = { ...raw };\n const kind = kindOf(property);\n\n const field: TapFormField = {\n name,\n label: readString(property, 'title') ?? humanise(name),\n kind,\n required: required.has(name),\n };\n\n const hint = readString(property, 'description');\n if (hint !== undefined) field.hint = hint;\n\n if (kind === 'enum' && Array.isArray(property.enum)) {\n field.options = property.enum\n .filter((option): option is string => typeof option === 'string')\n .map(option => ({ value: option, label: option }));\n }\n\n return [field];\n });\n}\n\ntype Values = Record<string, unknown>;\n\nfunction requiredErrors(\n fields: TapFormField[],\n values: Values,\n): Record<string, string> {\n const errors: Record<string, string> = {};\n for (const field of fields) {\n const value = values[field.name];\n if (field.required && (value === undefined || value === '')) {\n errors[field.name] = `${field.label} is required.`;\n }\n }\n return errors;\n}\n\nfunction compile(schema: JsonSchema): z.ZodType | null {\n try {\n return z.fromJSONSchema(schema);\n } catch {\n return null;\n }\n}\n\nfunction validate(\n fields: TapFormField[],\n compiled: z.ZodType | null,\n values: Values,\n): Record<string, string> {\n const errors = requiredErrors(fields, values);\n if (compiled === null) return errors;\n\n const result = compiled.safeParse(values);\n if (result.success) return errors;\n\n for (const issue of result.error.issues) {\n const key = issue.path[0];\n if (typeof key === 'string' && errors[key] === undefined) {\n errors[key] = issue.message;\n }\n }\n return errors;\n}\n\nfunction clean(fields: TapFormField[], values: Values): Values {\n const output: Values = {};\n for (const field of fields) {\n const value = values[field.name];\n if (value === undefined || value === '') continue;\n output[field.name] = value;\n }\n return output;\n}\n\n/** The props `TapForm` takes. */\nexport interface TapFormProps<T> {\n /** The object schema to render, one control per declared property. */\n schema: JsonSchema;\n /** The value the fields start at; later changes to it do not reset the form. */\n value?: Partial<T>;\n /** Defaults to `Submit`. */\n submitLabel?: string;\n /** Defaults to `Cancel`. */\n cancelLabel?: string;\n /** Marks the submit button busy and disables it while a submit is in flight. */\n busy?: boolean;\n /** Called with the cleaned value once validation passes. */\n onSubmit: (value: T) => void | Promise<void>;\n /** Renders a cancel button next to submit, and is called by it. */\n onCancel?: () => void;\n}\n\n/**\n * Renders a JSON Schema as a form in the host's styling, validates it on submit\n * and hands back the value.\n *\n * @remarks\n * Point it at the same schema file the manifest references, so the form and the\n * handler change together. Submit drops the fields left blank, then checks what\n * remains against the whole schema, so a `minimum`, `pattern` or `maxLength`\n * lands under the control it belongs to; a required field left empty is reported\n * without reaching the schema. A schema this SDK's Zod version cannot compile\n * still renders and still enforces required fields. `onSubmit` runs only when\n * nothing failed, and the form holds its own values, so a change to `value` after\n * the first render does not reset it. A form with no `value` has nothing to infer\n * `T` from, so annotate the handler's parameter.\n *\n * @example\n * ```tsx\n * import { TapForm, type TapSettingsProps } from '@tappify/extension-sdk';\n *\n * const schema = {\n * type: 'object',\n * properties: { refreshMinutes: { type: 'integer', title: 'Refresh minutes' } },\n * required: ['refreshMinutes'],\n * };\n *\n * function Settings({ values, onChange }: TapSettingsProps) {\n * return (\n * <TapForm\n * schema={schema}\n * value={values}\n * submitLabel=\"Save\"\n * onSubmit={next => onChange(next)}\n * />\n * );\n * }\n * ```\n */\nexport function TapForm<T>({\n schema,\n value,\n submitLabel = 'Submit',\n cancelLabel = 'Cancel',\n busy = false,\n onSubmit,\n onCancel,\n}: TapFormProps<T>): ReactElement {\n const fields = useMemo(() => formFields(schema), [schema]);\n const compiled = useMemo(() => compile(schema), [schema]);\n const [values, setValues] = useState<Values>(() => ({ ...value }));\n const [errors, setErrors] = useState<Record<string, string>>({});\n\n const set = (name: string, next: unknown): void => {\n setValues(current => ({ ...current, [name]: next }));\n setErrors(current => {\n if (current[name] === undefined) return current;\n const { [name]: _cleared, ...rest } = current;\n return rest;\n });\n };\n\n const submit: SubmitEventHandler<HTMLFormElement> = event => {\n event.preventDefault();\n const candidate = clean(fields, values);\n const found = validate(fields, compiled, candidate);\n setErrors(found);\n if (Object.keys(found).length > 0) return;\n\n void onSubmit(candidate as T);\n };\n\n return (\n <form className=\"tap-form\" onSubmit={submit} noValidate>\n {fields.map(field => {\n const raw = values[field.name];\n\n if (field.kind === 'enum') {\n return (\n <TapSelect\n key={field.name}\n label={field.label}\n hint={field.hint}\n error={errors[field.name]}\n required={field.required}\n value={typeof raw === 'string' ? raw : ''}\n options={[\n { value: '', label: 'Choose one' },\n ...(field.options ?? []),\n ]}\n onChange={event => {\n set(field.name, event.target.value);\n }}\n />\n );\n }\n\n if (field.kind === 'boolean') {\n return (\n <TapInput\n key={field.name}\n type=\"checkbox\"\n label={field.label}\n hint={field.hint}\n error={errors[field.name]}\n className=\"tap-control--checkbox\"\n checked={raw === true}\n onChange={event => {\n set(field.name, event.target.checked);\n }}\n />\n );\n }\n\n if (field.kind === 'textarea') {\n return (\n <TapInput\n key={field.name}\n multiline\n label={field.label}\n hint={field.hint}\n error={errors[field.name]}\n required={field.required}\n value={typeof raw === 'string' ? raw : ''}\n onChange={event => {\n set(field.name, event.target.value);\n }}\n />\n );\n }\n\n return (\n <TapInput\n key={field.name}\n type={\n field.kind === 'number'\n ? 'number'\n : field.kind === 'date'\n ? 'date'\n : 'text'\n }\n label={field.label}\n hint={field.hint}\n error={errors[field.name]}\n required={field.required}\n value={\n typeof raw === 'string' || typeof raw === 'number'\n ? String(raw)\n : ''\n }\n onChange={event => {\n set(\n field.name,\n field.kind === 'number'\n ? event.target.value === ''\n ? ''\n : Number(event.target.value)\n : event.target.value,\n );\n }}\n />\n );\n })}\n\n <div className=\"tap-form__actions\">\n {onCancel !== undefined && (\n <TapButton variant=\"secondary\" onClick={onCancel}>\n {cancelLabel}\n </TapButton>\n )}\n <TapButton type=\"submit\" loading={busy}>\n {submitLabel}\n </TapButton>\n </div>\n </form>\n );\n}\n"],"mappings":"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","names":["_multiline","rest","useId","jsx","jsxs","useId","jsx","jsxs"]}
@@ -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 _chunk7YKH4LZNcjs = require('./chunk-7YKH4LZN.cjs');
3
+ var _chunkPYJMJF44cjs = require('./chunk-PYJMJF44.cjs');
4
4
 
5
5
 
6
6
 
7
- var _chunk6HETNZJYcjs = require('./chunk-6HETNZJY.cjs');
7
+ var _chunk2DBSVKAUcjs = require('./chunk-2DBSVKAU.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, _chunk6HETNZJYcjs.TapError)(
49
+ throw new (0, _chunk2DBSVKAUcjs.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, _chunk6HETNZJYcjs.TapError)(
58
+ throw new (0, _chunk2DBSVKAUcjs.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, _chunk6HETNZJYcjs.TapError)(
89
+ throw new (0, _chunk2DBSVKAUcjs.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,15 +124,10 @@ function readText(value, key) {
124
124
  // src/server/handler.ts
125
125
  var MAX_BODY_BYTES = 1048576;
126
126
  var WORK_OPERATIONS = [
127
- "configure",
128
127
  "list_containers",
129
- "describe_container",
130
128
  "create",
131
129
  "attach",
132
130
  "comment",
133
- "set_status",
134
- "hydrate_event",
135
- "disconnect",
136
131
  "status",
137
132
  "resolve"
138
133
  ];
@@ -188,7 +183,7 @@ function toCredentials(payload) {
188
183
  return credentials;
189
184
  }
190
185
  function tooLarge() {
191
- return new (0, _chunk6HETNZJYcjs.TapServerError)(
186
+ return new (0, _chunk2DBSVKAUcjs.TapServerError)(
192
187
  SERVER_ERROR_CODES.BODY_TOO_LARGE,
193
188
  "A Tappify request body is at most 1 MB. Return a cursor or an id the host can follow instead of inlining the payload.",
194
189
  413
@@ -205,7 +200,7 @@ async function readBody(request) {
205
200
  try {
206
201
  return JSON.parse(text);
207
202
  } catch (e2) {
208
- throw new (0, _chunk6HETNZJYcjs.TapServerError)(
203
+ throw new (0, _chunk2DBSVKAUcjs.TapServerError)(
209
204
  SERVER_ERROR_CODES.BODY_INVALID,
210
205
  "The request body was not JSON. Do not parse or re-encode the body before handing the Request to createTappifyHandler().",
211
206
  400
@@ -243,31 +238,24 @@ function resolveRoute(options, method, segments, url, payload) {
243
238
  }
244
239
  if (head === "work" && method === "POST" && name !== void 0) {
245
240
  const operation = WORK_OPERATIONS.find((candidate) => candidate === name);
246
- const handler = operation === void 0 ? void 0 : _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);
241
+ return route(
242
+ operation === void 0 ? void 0 : _optionalChain([options, 'access', _2 => _2.work, 'optionalAccess', _3 => _3[operation]]),
243
+ bodyInput
244
+ );
257
245
  }
258
246
  if (head === "events" && method === "POST") {
259
247
  const envelope = readRecord(payload, "event");
260
248
  const eventName = readText(envelope, "name");
261
249
  if (eventName === null) {
262
- throw new (0, _chunk6HETNZJYcjs.TapServerError)(
250
+ throw new (0, _chunk2DBSVKAUcjs.TapServerError)(
263
251
  SERVER_ERROR_CODES.BODY_INVALID,
264
252
  'The events body has no event.name. Post the envelope Tappify sends unchanged, with the event name under "event".',
265
253
  400
266
254
  );
267
255
  }
268
- const declared = _chunk7YKH4LZNcjs.EVENTS.find((candidate) => candidate === eventName);
256
+ const declared = _chunkPYJMJF44cjs.EVENTS.find((candidate) => candidate === eventName);
269
257
  return route(
270
- declared === void 0 ? void 0 : _optionalChain([options, 'access', _6 => _6.events, 'optionalAccess', _7 => _7[declared]]),
258
+ declared === void 0 ? void 0 : _optionalChain([options, 'access', _4 => _4.events, 'optionalAccess', _5 => _5[declared]]),
271
259
  void 0,
272
260
  _nullishCoalesce(readUnknown(envelope, "payload"), () => ( {})),
273
261
  true
@@ -347,10 +335,10 @@ function createTappifyHandler(options) {
347
335
  const result = await matched.handler(typed);
348
336
  return matched.noContent ? new Response(null, { status: 204 }) : json(result);
349
337
  } catch (error) {
350
- if (_chunk6HETNZJYcjs.TapServerError.is(error)) {
338
+ if (_chunk2DBSVKAUcjs.TapServerError.is(error)) {
351
339
  return wireError(error.code, error.message, _nullishCoalesce(error.status, () => ( 400)));
352
340
  }
353
- if (_chunk6HETNZJYcjs.TapError.is(error)) {
341
+ if (_chunk2DBSVKAUcjs.TapError.is(error)) {
354
342
  const tokenFailure = TOKEN_FAILURES.find(
355
343
  (candidate) => candidate === error.code
356
344
  );
@@ -373,6 +361,5 @@ function createTappifyHandler(options) {
373
361
 
374
362
 
375
363
 
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
364
+ 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.readRecord = readRecord; exports.readText = readText; exports.createTappifyHandler = createTappifyHandler;
365
+ //# sourceMappingURL=chunk-G7XZSN2Q.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["/home/runner/work/extension-sdk/extension-sdk/dist/chunk-G7XZSN2Q.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-G7XZSN2Q.cjs","sourcesContent":[null,"import type { TapErrorCode } from '../client/errors';\n\n/**\n * The codes `createTappifyHandler` answers with itself, keyed by a short name.\n *\n * @remarks\n * Every one of these comes from the SDK, not from your own handlers: a route or\n * handler that is not there, a token that is missing or does not verify, a missing\n * event id, a body that is not JSON or is over 1 MB, a webhook Tappify rejected,\n * and the catch-all for an error a handler let escape. Throw a `TapServerError`\n * with a code of your own for anything your server decides.\n */\nexport const SERVER_ERROR_CODES = {\n ROUTE_UNKNOWN: 'TAP_ROUTE_UNKNOWN',\n HANDLER_MISSING: 'TAP_HANDLER_MISSING',\n INTERNAL_ERROR: 'TAP_INTERNAL_ERROR',\n TOKEN_MISSING: 'TAP_TOKEN_MISSING',\n TOKEN_INVALID: 'TAP_TOKEN_INVALID',\n TOKEN_AUDIENCE: 'TAP_TOKEN_AUDIENCE',\n EVENT_ID_MISSING: 'TAP_EVENT_ID_MISSING',\n BODY_INVALID: 'TAP_BODY_INVALID',\n BODY_TOO_LARGE: 'TAP_BODY_TOO_LARGE',\n WEBHOOK_REJECTED: 'TAP_WEBHOOK_REJECTED',\n} as const satisfies Record<string, TapErrorCode>;\n\n/** Every code the handler answers with itself, as a union of its own values. */\nexport type TapServerErrorCode =\n (typeof SERVER_ERROR_CODES)[keyof typeof SERVER_ERROR_CODES];\n","import {\n createLocalJWKSet,\n createRemoteJWKSet,\n jwtVerify,\n type JWTPayload,\n type JWTVerifyGetKey,\n} from 'jose';\nimport { TapError } from '../client/errors';\nimport type { TappifyClaims, VerifyTokenOptions } from './types';\n\n/**\n * The key set `verifyTappifyToken` fetches when no `jwksUrl` or `jwks` is given.\n *\n * @remarks\n * This is production. Point `jwksUrl` at the environment that is calling you when\n * you run against another one, and pass `jwks` in tests.\n */\nexport const DEFAULT_JWKS_URL =\n 'https://api.tappify.ai/api/v1/extensions/.well-known/jwks.json';\n\nconst CLOCK_TOLERANCE_SECONDS = 60;\n\nconst remoteSets = new Map<string, JWTVerifyGetKey>();\n\nfunction keySet(options: VerifyTokenOptions): JWTVerifyGetKey {\n if (options.jwks) return createLocalJWKSet(options.jwks);\n\n const url = options.jwksUrl ?? DEFAULT_JWKS_URL;\n const cached = remoteSets.get(url);\n if (cached) return cached;\n\n const created = createRemoteJWKSet(new URL(url));\n remoteSets.set(url, created);\n return created;\n}\n\nfunction readString(payload: JWTPayload, key: string): string | undefined {\n const value = payload[key];\n return typeof value === 'string' ? value : undefined;\n}\n\nfunction toClaims(payload: JWTPayload, audience: string): TappifyClaims {\n const aud = payload.aud;\n const matches =\n aud === audience || (Array.isArray(aud) && aud.includes(audience));\n\n if (!matches) {\n throw new TapError(\n 'TAP_TOKEN_AUDIENCE',\n `This install token was issued for a different extension. Create the handler with the same extensionId as the \"id\" in tappify.extension.json, so its audience is \"${audience}\".`,\n );\n }\n\n const installId = readString(payload, 'installId');\n const extensionId = readString(payload, 'extensionId');\n const organizationId = readString(payload, 'organizationId');\n\n if (\n installId === undefined ||\n extensionId === undefined ||\n organizationId === undefined\n ) {\n throw new TapError(\n 'TAP_TOKEN_INVALID',\n `The install token is missing the installId, extensionId or organizationId claim. Verify against ${DEFAULT_JWKS_URL} and do not reuse tokens across environments.`,\n );\n }\n\n const scopes = payload.scopes;\n const userId = readString(payload, 'userId');\n\n const claims: TappifyClaims = {\n iss: readString(payload, 'iss') ?? '',\n aud: audience,\n exp: typeof payload.exp === 'number' ? payload.exp : 0,\n iat: typeof payload.iat === 'number' ? payload.iat : 0,\n installId,\n extensionId,\n projectId: readString(payload, 'projectId') ?? null,\n organizationId,\n scopes: Array.isArray(scopes)\n ? scopes.filter((scope): scope is string => typeof scope === 'string')\n : [],\n };\n\n if (userId !== undefined) claims.userId = userId;\n return claims;\n}\n\n/**\n * Verifies an install token against Tappify's signing keys and returns its\n * claims.\n *\n * @remarks\n * Checks the RS256 signature, the expiry with 60 seconds of clock tolerance, and\n * that the audience is `ext:<extensionId>`. Rejects with a `TapError` carrying\n * `TAP_TOKEN_INVALID` when the signature, the expiry or a required claim is wrong,\n * and `TAP_TOKEN_AUDIENCE` when the token was issued for another extension. A\n * remote key set is fetched once per url and cached for the process, so calling\n * this per request costs nothing after the first. `createTappifyHandler` already\n * verifies every route but health, so call this only outside the handler, or\n * where you accept a Tappify token on a route of your own.\n *\n * @example\n * ```ts\n * import { verifyTappifyToken } from '@tappify/extension-sdk/server';\n *\n * async function whoCalled(request: Request): Promise<string> {\n * const token = (request.headers.get('authorization') ?? '').replace('Bearer ', '');\n * const claims = await verifyTappifyToken(token, { extensionId: 'starter' });\n * return claims.installId;\n * }\n * ```\n */\nexport async function verifyTappifyToken(\n token: string,\n options: VerifyTokenOptions,\n): Promise<TappifyClaims> {\n const audience = `ext:${options.extensionId}`;\n\n let payload: JWTPayload;\n try {\n const verified = await jwtVerify(token, keySet(options), {\n algorithms: ['RS256'],\n clockTolerance: CLOCK_TOLERANCE_SECONDS,\n });\n payload = verified.payload;\n } catch {\n throw new TapError(\n 'TAP_TOKEN_INVALID',\n 'The install token did not verify. Check that your server reads the Authorization header unchanged, that its clock is correct, and that jwksUrl points at the Tappify environment that called you.',\n );\n }\n\n return toClaims(payload, audience);\n}\n","import type { TapServerErrorCode } from './codes';\n\nexport const INTERNAL_MESSAGE =\n 'The extension server failed to handle this call. Check your own logs; Tappify records the failure against your extension.';\n\nexport function json(data: unknown, status = 200): Response {\n return new Response(JSON.stringify(data), {\n status,\n headers: { 'content-type': 'application/json' },\n });\n}\n\nexport function wireError(\n code: string,\n message: string,\n status: number,\n): Response {\n return json({ error: { code, message } }, status);\n}\n\nexport function failure(\n code: TapServerErrorCode,\n message: string,\n status: number,\n): Response {\n return wireError(code, message, status);\n}\n\nexport function readUnknown(value: unknown, key: string): unknown {\n if (typeof value !== 'object' || value === null) return undefined;\n return (value as Record<string, unknown>)[key];\n}\n\nexport function readRecord(\n value: unknown,\n key: string,\n): Record<string, unknown> {\n const nested = readUnknown(value, key);\n return typeof nested === 'object' && nested !== null\n ? (nested as Record<string, unknown>)\n : {};\n}\n\nexport function readText(value: unknown, key: string): string | null {\n const nested = readUnknown(value, key);\n return typeof nested === 'string' ? nested : null;\n}\n","import { TapError, TapServerError } from '../client/errors';\nimport type { TapCredentialValues, TapDateRange } from '../client/types';\nimport { EVENTS } from '../manifest/constants';\nimport { SERVER_ERROR_CODES, type TapServerErrorCode } from './codes';\nimport { verifyTappifyToken } from './token';\nimport type {\n ContextBlock,\n MentionItems,\n MetricsInput,\n MetricsResponse,\n TappifyCallContext,\n TappifyClaims,\n TappifyDocuments,\n TappifyHandlerOptions,\n TappifyHealth,\n TappifyInstall,\n TappifyRequest,\n WorkOperation,\n} from './types';\nimport {\n INTERNAL_MESSAGE,\n failure,\n json,\n readRecord,\n readText,\n readUnknown,\n wireError,\n} from './wire';\n\n/**\n * What `createTappifyHandler` returns: a `fetch` handler, which is what Cloudflare\n * Workers, Vercel and the two adapters all take.\n */\nexport type TappifyFetchHandler = (request: Request) => Promise<Response>;\n\nconst MAX_BODY_BYTES = 1_048_576;\n\nconst WORK_OPERATIONS: readonly WorkOperation[] = [\n 'list_containers',\n 'create',\n 'attach',\n 'comment',\n 'status',\n 'resolve',\n];\n\nconst TOKEN_FAILURES: readonly TapServerErrorCode[] = [\n SERVER_ERROR_CODES.TOKEN_INVALID,\n SERVER_ERROR_CODES.TOKEN_AUDIENCE,\n];\n\n/**\n * The token is signed by Tappify and the body is not, so the claims win wherever\n * they carry a value and the body only fills what the token leaves out.\n */\nfunction toInstall(payload: unknown, claims: TappifyClaims): TappifyInstall {\n const raw = readRecord(payload, 'install');\n return {\n id: claims.installId,\n extensionId: claims.extensionId,\n projectId: claims.projectId ?? readText(raw, 'projectId'),\n organizationId: claims.organizationId,\n };\n}\n\nfunction toPlatform(value: string | null): TappifyCallContext['platform'] {\n return value === 'ios' || value === 'android' ? value : 'all';\n}\n\nfunction toPreset(value: string | null): TapDateRange['preset'] {\n return value === '7d' || value === '30d' || value === '90d'\n ? value\n : undefined;\n}\n\nfunction toContext(payload: unknown): TappifyCallContext {\n const raw = readRecord(payload, 'context');\n const filters = readRecord(raw, 'filters');\n const rawRange = readRecord(filters, 'range');\n const from = readText(raw, 'from') ?? readText(rawRange, 'from') ?? '';\n const to = readText(raw, 'to') ?? readText(rawRange, 'to') ?? '';\n\n const range: TapDateRange = {\n from: readText(rawRange, 'from') ?? from,\n to: readText(rawRange, 'to') ?? to,\n };\n\n const preset = toPreset(readText(rawRange, 'preset'));\n if (preset !== undefined) range.preset = preset;\n\n return {\n projectId: readText(raw, 'projectId'),\n platform: toPlatform(readText(raw, 'platform')),\n from,\n to,\n filters: {\n range,\n platform: toPlatform(readText(filters, 'platform')),\n country: readText(filters, 'country') ?? 'all',\n },\n };\n}\n\nfunction toCredentials(payload: unknown): TapCredentialValues {\n const raw = readRecord(payload, 'credentials');\n const credentials: Record<string, string> = {};\n for (const [name, value] of Object.entries(raw)) {\n if (typeof value === 'string') credentials[name] = value;\n }\n return credentials;\n}\n\nfunction tooLarge(): TapServerError {\n return new TapServerError(\n SERVER_ERROR_CODES.BODY_TOO_LARGE,\n 'A Tappify request body is at most 1 MB. Return a cursor or an id the host can follow instead of inlining the payload.',\n 413,\n );\n}\n\nasync function readBody(request: Request): Promise<unknown> {\n if (request.method === 'GET' || request.method === 'HEAD') return {};\n\n const length = request.headers.get('content-length');\n if (length !== null && Number(length) > MAX_BODY_BYTES) throw tooLarge();\n\n const bytes = await request.arrayBuffer();\n if (bytes.byteLength > MAX_BODY_BYTES) throw tooLarge();\n if (bytes.byteLength === 0) return {};\n\n const text = new TextDecoder().decode(bytes);\n try {\n return JSON.parse(text);\n } catch {\n throw new TapServerError(\n SERVER_ERROR_CODES.BODY_INVALID,\n 'The request body was not JSON. Do not parse or re-encode the body before handing the Request to createTappifyHandler().',\n 400,\n );\n }\n}\n\n/**\n * The route table holds handlers whose declared input and event types the\n * manifest augmentations narrow. Method syntax keeps the parameter bivariant,\n * so a narrowed handler still fits a table the wire types as `unknown`.\n */\ntype AnyRouteHandler = {\n route(request: TappifyRequest<unknown, unknown>): unknown;\n}['route'];\n\ninterface Route {\n handler: AnyRouteHandler | undefined;\n input: unknown;\n event: unknown;\n noContent: boolean;\n}\n\nfunction route(\n handler: AnyRouteHandler | undefined,\n input: unknown,\n event?: unknown,\n noContent = false,\n): Route {\n return { handler, input, event, noContent };\n}\n\nfunction handlerFor(\n map: Partial<Record<string, AnyRouteHandler>> | undefined,\n name: string,\n): AnyRouteHandler | undefined {\n return map?.[name];\n}\n\nfunction resolveRoute(\n options: TappifyHandlerOptions,\n method: string,\n segments: string[],\n url: URL,\n payload: unknown,\n): Route | null {\n const [head, name] = segments;\n const bodyInput = readUnknown(payload, 'input') ?? {};\n\n if (head === 'metrics' && method === 'POST') {\n return route(options.metrics, bodyInput);\n }\n if (head === 'tools' && method === 'POST' && name !== undefined) {\n return route(handlerFor(options.tools, name), bodyInput);\n }\n if (head === 'mentions' && method === 'GET' && name !== undefined) {\n return route(handlerFor(options.mentions, name), {\n q: url.searchParams.get('q') ?? '',\n });\n }\n if (head === 'actions' && method === 'POST' && name !== undefined) {\n return route(handlerFor(options.actions, name), bodyInput);\n }\n if (head === 'procedures' && method === 'POST' && name !== undefined) {\n return route(handlerFor(options.procedures, name), bodyInput);\n }\n if (head === 'context' && method === 'GET' && name !== undefined) {\n return route(handlerFor(options.context, name), undefined);\n }\n if (head === 'work' && method === 'POST' && name !== undefined) {\n const operation = WORK_OPERATIONS.find(candidate => candidate === name);\n return route(\n operation === undefined ? undefined : options.work?.[operation],\n bodyInput,\n );\n }\n if (head === 'events' && method === 'POST') {\n const envelope = readRecord(payload, 'event');\n const eventName = readText(envelope, 'name');\n if (eventName === null) {\n throw new TapServerError(\n SERVER_ERROR_CODES.BODY_INVALID,\n 'The events body has no event.name. Post the envelope Tappify sends unchanged, with the event name under \"event\".',\n 400,\n );\n }\n const declared = EVENTS.find(candidate => candidate === eventName);\n return route(\n declared === undefined ? undefined : options.events?.[declared],\n undefined,\n readUnknown(envelope, 'payload') ?? {},\n true,\n );\n }\n\n return null;\n}\n\n/**\n * Builds the one `fetch` handler that answers every route Tappify calls on your\n * server.\n *\n * @remarks\n * It routes the paths under `/tappify`, minus `basePath` when your framework mounts\n * it under a prefix, and hands each matched handler a `TappifyRequest`. Every\n * route but `GET /tappify/health` needs a bearer install token, which it verifies\n * with `verifyTappifyToken`, and an `X-Tappify-Event-Id` header; a missing token is\n * 401 `TAP_TOKEN_MISSING`, a missing id 400 `TAP_EVENT_ID_MISSING`, an unknown path\n * 404 `TAP_ROUTE_UNKNOWN` and a declared name with no handler 404\n * `TAP_HANDLER_MISSING`. A body that is not JSON is 400 `TAP_BODY_INVALID` and one\n * over 1 MB is 413 `TAP_BODY_TOO_LARGE`. A `TapServerError` a handler throws is\n * answered with its own code and status, and a `TapError` other than the two token\n * failures with its own code and 400; anything else becomes 500\n * `TAP_INTERNAL_ERROR` with a fixed message, so an internal failure never reaches\n * the owner. An events route answers 204 with no body, and every other route\n * answers the handler's return value as JSON. Export the options object as well as\n * the handler: `createTestClient` takes the options.\n *\n * @example\n * ```ts\n * import {\n * createTappifyHandler,\n * type TappifyHandlerOptions,\n * } from '@tappify/extension-sdk/server';\n *\n * const handlerOptions: TappifyHandlerOptions = {\n * extensionId: 'starter',\n * health: () => ({ ok: true, version: '0.1.0' }),\n * procedures: {\n * getSummary: () => ({ installs: 1260, delta: 0.087, topKeyword: 'photo editor' }),\n * },\n * };\n *\n * const handler = createTappifyHandler(handlerOptions);\n * ```\n */\nexport function createTappifyHandler(\n options: TappifyHandlerOptions,\n): TappifyFetchHandler {\n const basePath = options.basePath ?? '';\n\n return async function handle(request: Request): Promise<Response> {\n const url = new URL(request.url);\n const path = url.pathname.startsWith(basePath)\n ? url.pathname.slice(basePath.length)\n : url.pathname;\n\n const segments = path.split('/').filter(segment => segment !== '');\n if (segments[0] !== 'tappify') {\n return failure(\n SERVER_ERROR_CODES.ROUTE_UNKNOWN,\n `Nothing is mounted at ${path}. Tappify calls paths under /tappify; set basePath if your framework mounts the handler under a prefix.`,\n 404,\n );\n }\n\n const rest = segments.slice(1);\n\n if (rest[0] === 'health' && request.method === 'GET') {\n const health: TappifyHealth = options.health\n ? await options.health()\n : { ok: true };\n return json(health);\n }\n\n const authorization = request.headers.get('authorization') ?? '';\n const token = authorization.startsWith('Bearer ')\n ? authorization.slice('Bearer '.length)\n : '';\n\n if (token === '') {\n return failure(\n SERVER_ERROR_CODES.TOKEN_MISSING,\n 'This route needs an install token. Tappify sends it as Authorization: Bearer <token>; forward the header unchanged from your framework.',\n 401,\n );\n }\n\n const eventId = request.headers.get('x-tappify-event-id');\n if (eventId === null || eventId === '') {\n return failure(\n SERVER_ERROR_CODES.EVENT_ID_MISSING,\n 'Every Tappify call carries X-Tappify-Event-Id for idempotency. Forward the header unchanged, and use it as the key when you deduplicate.',\n 400,\n );\n }\n\n try {\n const claims = await verifyTappifyToken(token, {\n extensionId: options.extensionId,\n jwksUrl: options.jwksUrl,\n jwks: options.jwks,\n });\n\n const payload = await readBody(request);\n const matched = resolveRoute(options, request.method, rest, url, payload);\n\n if (matched === null) {\n return failure(\n SERVER_ERROR_CODES.ROUTE_UNKNOWN,\n `Tappify has no ${request.method} ${path} route. The paths it calls are listed under \"Vendor server endpoints\" in the reference; check the method and the spelling.`,\n 404,\n );\n }\n\n if (matched.handler === undefined) {\n return failure(\n SERVER_ERROR_CODES.HANDLER_MISSING,\n `No handler is registered for ${request.method} ${path}. Add it to createTappifyHandler(), or remove the contribution from tappify.extension.json.`,\n 404,\n );\n }\n\n const documents = readRecord(payload, 'documents');\n\n const typed: TappifyRequest<unknown, unknown> = {\n install: toInstall(payload, claims),\n context: toContext(payload),\n input: matched.input,\n credentials: toCredentials(payload),\n documents: documents as TappifyDocuments,\n event: matched.event,\n eventId,\n claims,\n request,\n };\n\n const result = await matched.handler(typed);\n return matched.noContent\n ? new Response(null, { status: 204 })\n : json(result);\n } catch (error) {\n if (TapServerError.is(error)) {\n return wireError(error.code, error.message, error.status ?? 400);\n }\n if (TapError.is(error)) {\n const tokenFailure = TOKEN_FAILURES.find(\n candidate => candidate === error.code,\n );\n if (tokenFailure !== undefined) {\n return failure(tokenFailure, error.message, 401);\n }\n return wireError(error.code, error.message, 400);\n }\n return failure(SERVER_ERROR_CODES.INTERNAL_ERROR, INTERNAL_MESSAGE, 500);\n }\n };\n}\n\nexport type {\n ContextBlock,\n MentionItems,\n MetricsInput,\n MetricsResponse,\n TappifyClaims,\n};\n"]}
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  TapContext
3
- } from "./chunk-QA5AL3VZ.mjs";
3
+ } from "./chunk-QHM7XNRK.mjs";
4
4
 
5
5
  // src/host/provider.tsx
6
6
  import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
@@ -23,4 +23,4 @@ export {
23
23
  TapHostProvider,
24
24
  createHostQueryClient
25
25
  };
26
- //# sourceMappingURL=chunk-RBIQT75L.mjs.map
26
+ //# sourceMappingURL=chunk-GG3IX52W.mjs.map
@@ -1,20 +1,24 @@
1
1
  import {
2
2
  TapError
3
- } from "./chunk-QXD7WVKK.mjs";
3
+ } from "./chunk-CSTYBKZC.mjs";
4
4
 
5
5
  // src/testing/environment.ts
6
6
  var HOST_THEME = {
7
- "--bg-paper": "#fdfdfd",
8
- "--bg-canvas": "#f2f3ee",
9
- "--bg-light-gray": "#edf1f5",
10
- "--fg-primary": "#121212",
11
- "--fg-sub": "rgba(18, 18, 18, 0.7)",
12
- "--divider": "#d5d7da",
13
- "--lime": "#b4d773",
14
- "--deep-green": "#234e20",
15
- "--danger": "#b42318",
16
- "--on-accent": "#ffffff",
17
- "--font-one-ui": "'Inter Variable', 'Inter', system-ui, sans-serif"
7
+ "--bg-base": "#ffffff",
8
+ "--bg-subtle": "rgba(18, 18, 18, 0.04)",
9
+ "--bg-raised": "#ffffff",
10
+ "--fg-default": "#121212",
11
+ "--fg-muted": "rgba(18, 18, 18, 0.6)",
12
+ "--fg-inverse": "#ffffff",
13
+ "--tap-accent": "#4f46e5",
14
+ "--tap-accent-fg": "#ffffff",
15
+ "--tap-danger": "#b42318",
16
+ "--tap-scrim": "rgba(18, 18, 18, 0.7)",
17
+ "--tap-radius": "6px",
18
+ "--tap-space": "8px",
19
+ "--font-sans": "system-ui, -apple-system, sans-serif",
20
+ "--font-mono": "ui-monospace, SFMono-Regular, monospace",
21
+ "--divider": "rgba(18, 18, 18, 0.12)"
18
22
  };
19
23
  function installHostTheme(root) {
20
24
  for (const [name, value] of Object.entries(HOST_THEME)) {
@@ -135,4 +139,4 @@ export {
135
139
  tapMatchers,
136
140
  registerTapMatchers
137
141
  };
138
- //# sourceMappingURL=chunk-SLGOHIBL.mjs.map
142
+ //# sourceMappingURL=chunk-IGK2AP6J.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/testing/environment.ts","../src/testing/matchers.ts"],"sourcesContent":["const HOST_THEME: Record<string, string> = {\n '--bg-base': '#ffffff',\n '--bg-subtle': 'rgba(18, 18, 18, 0.04)',\n '--bg-raised': '#ffffff',\n '--fg-default': '#121212',\n '--fg-muted': 'rgba(18, 18, 18, 0.6)',\n '--fg-inverse': '#ffffff',\n '--tap-accent': '#4f46e5',\n '--tap-accent-fg': '#ffffff',\n '--tap-danger': '#b42318',\n '--tap-scrim': 'rgba(18, 18, 18, 0.7)',\n '--tap-radius': '6px',\n '--tap-space': '8px',\n '--font-sans': 'system-ui, -apple-system, sans-serif',\n '--font-mono': 'ui-monospace, SFMono-Regular, monospace',\n '--divider': 'rgba(18, 18, 18, 0.12)',\n};\n\n/**\n * Sets the host's fifteen CSS variables on an element, so a component renders in\n * a test the way it renders in the host.\n *\n * @remarks\n * Sets the light values as inline custom properties, which inherit into everything\n * below the element. The vitest and jest presets call it on\n * `document.documentElement` for you, so call it yourself only in a test that\n * renders outside `renderWithTap` or wants the variables on one subtree. It sets\n * no dark values; drive the mode through `renderWithTap`'s `theme` option, which\n * is what `useTapTheme` reads.\n *\n * @example\n * ```ts\n * import { installHostTheme } from '@tappify/extension-sdk/testing';\n *\n * function mountRoot(): HTMLElement {\n * const root = document.createElement('div');\n * installHostTheme(root);\n * document.body.append(root);\n * return root;\n * }\n * ```\n */\nexport function installHostTheme(root: HTMLElement): void {\n for (const [name, value] of Object.entries(HOST_THEME)) {\n root.style.setProperty(name, value);\n }\n}\n\nexport function clearTapPortals(root: ParentNode = document): void {\n for (const node of root.querySelectorAll('[data-tap-portal]')) node.remove();\n}\n","import { TapError } from '../client/errors';\nimport type { TapMock } from './mock';\n\n/** What each matcher returns, in the shape vitest and jest both read. */\nexport interface MatcherResult {\n pass: boolean;\n /** Built only when the runner reports, and worded for both `expect` and `not`. */\n message(): string;\n}\n\n/** The part of a runner's `expect` that `registerTapMatchers` needs. */\nexport interface MatcherTarget {\n /** Both runners take a map of matcher functions keyed by matcher name. */\n extend(matchers: Record<string, unknown>): void;\n}\n\nfunction isMock(value: unknown): value is TapMock {\n return (\n typeof value === 'object' &&\n value !== null &&\n 'calls' in value &&\n 'documents' in value &&\n typeof value.documents === 'function'\n );\n}\n\nfunction requireMock(value: unknown, matcher: string): TapMock {\n if (isMock(value)) return value;\n throw new TapError(\n 'TAP_TEST_TARGET_INVALID',\n `${matcher}() takes the mock renderWithTap() returned. Call it as expect(mock).${matcher}(…), not on the component or the tap object.`,\n );\n}\n\nfunction tagOf(value: unknown): string {\n return Object.prototype.toString.call(value);\n}\n\nfunction sameList(left: unknown[], right: unknown[]): boolean {\n return (\n left.length === right.length &&\n left.every((item, index) => same(item, right[index]))\n );\n}\n\nfunction bytesOf(view: ArrayBufferView): number[] {\n return [...new Uint8Array(view.buffer, view.byteOffset, view.byteLength)];\n}\n\nfunction same(left: unknown, right: unknown): boolean {\n if (Object.is(left, right)) return true;\n\n const tag = tagOf(left);\n if (tag !== tagOf(right)) return false;\n\n if (\n typeof left !== 'object' ||\n typeof right !== 'object' ||\n left === null ||\n right === null\n ) {\n return false;\n }\n\n if (left instanceof Date && right instanceof Date) {\n return left.valueOf() === right.valueOf();\n }\n if (left instanceof RegExp && right instanceof RegExp) {\n return left.source === right.source && left.flags === right.flags;\n }\n if (left instanceof Map && right instanceof Map) {\n return sameList([...left.entries()], [...right.entries()]);\n }\n if (left instanceof Set && right instanceof Set) {\n return sameList([...left.values()], [...right.values()]);\n }\n if (ArrayBuffer.isView(left) && ArrayBuffer.isView(right)) {\n return sameList(bytesOf(left), bytesOf(right));\n }\n if (Array.isArray(left) && Array.isArray(right)) {\n return sameList(left, right);\n }\n if (tag !== '[object Object]') return false;\n\n const keys = Object.keys(left);\n return (\n keys.length === Object.keys(right).length &&\n keys.every(\n key =>\n key in right && same(Reflect.get(left, key), Reflect.get(right, key)),\n )\n );\n}\n\n/**\n * The four matchers that assert on what a component asked the host to do.\n *\n * @remarks\n * Each takes the mock `renderWithTap` returns, so the assertion is\n * `expect(mock).toHaveToasted(…)` rather than `expect(component)`; anything else\n * throws a `TapError` carrying `TAP_TEST_TARGET_INVALID`. A failure names every\n * call that was recorded, so a wrong path or message reads without a debugger. The\n * vitest and jest presets register these, and both declare the four on the runner's\n * own `Matchers` interface, so `expect(mock).toHaveToasted` typechecks once the\n * preset is in the setup files.\n *\n * @example\n * ```ts\n * import '@tappify/extension-sdk/testing/vitest';\n * import { createTapMock } from '@tappify/extension-sdk/testing';\n * import { expect, it } from 'vitest';\n *\n * it('toasts when a release ships', () => {\n * const mock = createTapMock();\n * mock.tap.ui.toast('A release shipped', 'neutral');\n * expect(mock).toHaveToasted('A release shipped');\n * });\n * ```\n */\nexport const tapMatchers = {\n /** Passes when `tap.nav.push` was called with exactly that path. */\n toHaveNavigatedTo(received: unknown, path: string): MatcherResult {\n const mock = requireMock(received, 'toHaveNavigatedTo');\n const pass = mock.calls.navigations.includes(path);\n return {\n pass,\n message: () =>\n pass\n ? `Expected no navigation to ${path}, but tap.nav.push was called with it.`\n : `Expected a navigation to ${path}. tap.nav.push was called with: ${\n mock.calls.navigations.length === 0\n ? 'nothing'\n : mock.calls.navigations.join(', ')\n }.`,\n };\n },\n\n /** Passes when a toast matched the string exactly, or the pattern anywhere. */\n toHaveToasted(received: unknown, expected: string | RegExp): MatcherResult {\n const mock = requireMock(received, 'toHaveToasted');\n const messages = mock.calls.toasts.map(toast => toast.message);\n const pass = messages.some(message =>\n typeof expected === 'string'\n ? message === expected\n : expected.test(message),\n );\n return {\n pass,\n message: () =>\n pass\n ? `Expected no toast matching ${String(expected)}, but one was shown.`\n : `Expected a toast matching ${String(expected)}. Toasts shown: ${\n messages.length === 0 ? 'none' : messages.join(' | ')\n }.`,\n };\n },\n\n /**\n * Passes when `tap.actions.run` was called with that action, and with input\n * deep-equal to `input` when one is given.\n */\n toHaveRunAction(\n received: unknown,\n actionId: string,\n input?: unknown,\n ): MatcherResult {\n const mock = requireMock(received, 'toHaveRunAction');\n const runs = mock.calls.actions.filter(run => run.actionId === actionId);\n const pass =\n input === undefined\n ? runs.length > 0\n : runs.some(run => same(run.input, input));\n return {\n pass,\n message: () =>\n pass\n ? `Expected tap.actions.run(\"${actionId}\") not to be called.`\n : `Expected tap.actions.run(\"${actionId}\"${\n input === undefined ? '' : `, ${JSON.stringify(input)}`\n }). Runs recorded: ${\n mock.calls.actions.length === 0\n ? 'none'\n : JSON.stringify(mock.calls.actions)\n }.`,\n };\n },\n\n /**\n * Passes when the mock's storage holds a document deep-equal to that one in\n * that collection.\n */\n toHaveStored(\n received: unknown,\n collection: string,\n document: unknown,\n ): MatcherResult {\n const mock = requireMock(received, 'toHaveStored');\n const stored = mock.documents(collection);\n const pass = stored.some(candidate => same(candidate, document));\n return {\n pass,\n message: () =>\n pass\n ? `Expected \"${collection}\" not to hold ${JSON.stringify(document)}.`\n : `Expected \"${collection}\" to hold ${JSON.stringify(document)}. It holds: ${\n stored.length === 0 ? 'nothing' : JSON.stringify(stored)\n }.`,\n };\n },\n};\n\n/**\n * Registers the four matchers on a runner's `expect`.\n *\n * @remarks\n * The vitest and jest presets call this, so a suite that loads\n * `@tappify/extension-sdk/testing/vitest` or `.../testing/jest` as a setup file\n * needs nothing else. Call it yourself only in a suite that does not use a preset,\n * and once — registering twice replaces the same four names. It adds no types of\n * its own, so a suite without a preset declares the four on the runner's `Matchers`\n * interface itself.\n *\n * @example\n * ```ts\n * import { registerTapMatchers } from '@tappify/extension-sdk/testing';\n * import { expect } from 'vitest';\n *\n * registerTapMatchers(expect);\n * ```\n */\nexport function registerTapMatchers(target: MatcherTarget): void {\n target.extend(tapMatchers);\n}\n"],"mappings":"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","names":["document"]}
@@ -1,6 +1,6 @@
1
1
  "use strict";Object.defineProperty(exports, "__esModule", {value: true});
2
2
 
3
- var _chunkPRCZMOLDcjs = require('./chunk-PRCZMOLD.cjs');
3
+ var _chunkOPPYEA7Fcjs = require('./chunk-OPPYEA7F.cjs');
4
4
 
5
5
  // src/host/provider.tsx
6
6
  var _reactquery = require('@tanstack/react-query');
@@ -10,7 +10,7 @@ function TapHostProvider({
10
10
  children,
11
11
  queryClient
12
12
  }) {
13
- const provided = /* @__PURE__ */ _jsxruntime.jsx.call(void 0, _chunkPRCZMOLDcjs.TapContext.Provider, { value: tap, children });
13
+ const provided = /* @__PURE__ */ _jsxruntime.jsx.call(void 0, _chunkOPPYEA7Fcjs.TapContext.Provider, { value: tap, children });
14
14
  return queryClient === void 0 ? provided : /* @__PURE__ */ _jsxruntime.jsx.call(void 0, _reactquery.QueryClientProvider, { client: queryClient, children: provided });
15
15
  }
16
16
  function createHostQueryClient() {
@@ -23,4 +23,4 @@ function createHostQueryClient() {
23
23
 
24
24
 
25
25
  exports.TapHostProvider = TapHostProvider; exports.createHostQueryClient = createHostQueryClient;
26
- //# sourceMappingURL=chunk-VV7ELNRW.cjs.map
26
+ //# sourceMappingURL=chunk-JMUK3K47.cjs.map
@@ -1 +1 @@
1
- {"version":3,"sources":["/home/runner/work/extension-sdk/extension-sdk/dist/chunk-VV7ELNRW.cjs","../src/host/provider.tsx"],"names":[],"mappings":"AAAA;AACE;AACF,wDAA6B;AAC7B;AACA;ACJA,mDAAiD;AAsB7C,+CAAA;AANG,SAAS,eAAA,CAAgB;AAAA,EAC9B,GAAA;AAAA,EACA,QAAA;AAAA,EACA;AACF,CAAA,EAAuC;AACrC,EAAA,MAAM,SAAA,kBACJ,6BAAA,4BAAC,CAAW,QAAA,EAAX,EAAoB,KAAA,EAAO,GAAA,EAAM,SAAA,CAAS,CAAA;AAG7C,EAAA,OAAO,YAAA,IAAgB,KAAA,EAAA,EACrB,SAAA,kBAEA,6BAAA,+BAAC,EAAA,EAAoB,MAAA,EAAQ,WAAA,EAAc,QAAA,EAAA,SAAA,CAAS,CAAA;AAExD;AAGO,SAAS,qBAAA,CAAA,EAAqC;AACnD,EAAA,OAAO,IAAI,4BAAA,CAAY;AAAA,IACrB,cAAA,EAAgB,EAAE,OAAA,EAAS,EAAE,KAAA,EAAO,MAAM,EAAE;AAAA,EAC9C,CAAC,CAAA;AACH;ADjBA;AACA;AACE;AACA;AACF,iGAAC","file":"/home/runner/work/extension-sdk/extension-sdk/dist/chunk-VV7ELNRW.cjs","sourcesContent":[null,"import { QueryClient, QueryClientProvider } from '@tanstack/react-query';\nimport type { ReactElement, ReactNode } from 'react';\nimport { TapContext } from '../client/context';\nimport type { Tap } from '../client/types';\n\nexport interface TapHostProviderProps {\n tap: Tap;\n children: ReactNode;\n /**\n * A host that already renders a QueryClientProvider leaves this out; a\n * standalone host such as the local preview passes one from\n * `createHostQueryClient()` so the data hooks find a client.\n */\n queryClient?: QueryClient;\n}\n\nexport function TapHostProvider({\n tap,\n children,\n queryClient,\n}: TapHostProviderProps): ReactElement {\n const provided = (\n <TapContext.Provider value={tap}>{children}</TapContext.Provider>\n );\n\n return queryClient === undefined ? (\n provided\n ) : (\n <QueryClientProvider client={queryClient}>{provided}</QueryClientProvider>\n );\n}\n\n/** The query client a standalone host hands to `TapHostProvider`; reads never retry. */\nexport function createHostQueryClient(): QueryClient {\n return new QueryClient({\n defaultOptions: { queries: { retry: false } },\n });\n}\n"]}
1
+ {"version":3,"sources":["/home/runner/work/extension-sdk/extension-sdk/dist/chunk-JMUK3K47.cjs","../src/host/provider.tsx"],"names":[],"mappings":"AAAA;AACE;AACF,wDAA6B;AAC7B;AACA;ACJA,mDAAiD;AAsB7C,+CAAA;AANG,SAAS,eAAA,CAAgB;AAAA,EAC9B,GAAA;AAAA,EACA,QAAA;AAAA,EACA;AACF,CAAA,EAAuC;AACrC,EAAA,MAAM,SAAA,kBACJ,6BAAA,4BAAC,CAAW,QAAA,EAAX,EAAoB,KAAA,EAAO,GAAA,EAAM,SAAA,CAAS,CAAA;AAG7C,EAAA,OAAO,YAAA,IAAgB,KAAA,EAAA,EACrB,SAAA,kBAEA,6BAAA,+BAAC,EAAA,EAAoB,MAAA,EAAQ,WAAA,EAAc,QAAA,EAAA,SAAA,CAAS,CAAA;AAExD;AAGO,SAAS,qBAAA,CAAA,EAAqC;AACnD,EAAA,OAAO,IAAI,4BAAA,CAAY;AAAA,IACrB,cAAA,EAAgB,EAAE,OAAA,EAAS,EAAE,KAAA,EAAO,MAAM,EAAE;AAAA,EAC9C,CAAC,CAAA;AACH;ADjBA;AACA;AACE;AACA;AACF,iGAAC","file":"/home/runner/work/extension-sdk/extension-sdk/dist/chunk-JMUK3K47.cjs","sourcesContent":[null,"import { QueryClient, QueryClientProvider } from '@tanstack/react-query';\nimport type { ReactElement, ReactNode } from 'react';\nimport { TapContext } from '../client/context';\nimport type { Tap } from '../client/types';\n\nexport interface TapHostProviderProps {\n tap: Tap;\n children: ReactNode;\n /**\n * A host that already renders a QueryClientProvider leaves this out; a\n * standalone host such as the local preview passes one from\n * `createHostQueryClient()` so the data hooks find a client.\n */\n queryClient?: QueryClient;\n}\n\nexport function TapHostProvider({\n tap,\n children,\n queryClient,\n}: TapHostProviderProps): ReactElement {\n const provided = (\n <TapContext.Provider value={tap}>{children}</TapContext.Provider>\n );\n\n return queryClient === undefined ? (\n provided\n ) : (\n <QueryClientProvider client={queryClient}>{provided}</QueryClientProvider>\n );\n}\n\n/** The query client a standalone host hands to `TapHostProvider`; reads never retry. */\nexport function createHostQueryClient(): QueryClient {\n return new QueryClient({\n defaultOptions: { queries: { retry: false } },\n });\n}\n"]}
@@ -35,13 +35,6 @@ 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
- },
45
38
  {
46
39
  key: "crashes:read",
47
40
  label: "Read crash data",
@@ -115,7 +108,7 @@ var SCOPES = [
115
108
  {
116
109
  key: "work:sync",
117
110
  label: "Sync work items",
118
- description: "Sync comments and status on created work items",
111
+ description: "Two-way comments and status on created work items",
119
112
  requiresSecurityReview: false,
120
113
  requiresJustification: true
121
114
  },
@@ -306,4 +299,4 @@ export {
306
299
  findTable,
307
300
  slotsForPage
308
301
  };
309
- //# sourceMappingURL=chunk-SGCYEEXD.mjs.map
302
+ //# sourceMappingURL=chunk-MNDVYPEP.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/manifest/constants.ts"],"sourcesContent":["import type { ToolDeclaration } from './types';\n\nexport const SCOPES = [\n {\n key: 'ui:render',\n label: 'Render UI',\n description:\n 'Render the declared pages, tabs, widgets, row actions and settings panel, and open chat with a visible prompt',\n requiresSecurityReview: false,\n requiresJustification: false,\n },\n {\n key: 'projects:read',\n label: 'Read the project',\n description: 'Project name, platforms and store identity',\n requiresSecurityReview: false,\n requiresJustification: false,\n },\n {\n key: 'analytics:read',\n label: 'Read analytics',\n description: 'Downloads, impressions, page views and conversion series',\n requiresSecurityReview: false,\n requiresJustification: false,\n },\n {\n key: 'store.metadata:read',\n label: 'Read store metadata',\n description: 'Listing metadata, keyword set and keyword positions',\n requiresSecurityReview: false,\n requiresJustification: false,\n },\n {\n key: 'reviews:read',\n label: 'Read store reviews',\n description: 'Review text, ratings and replies',\n requiresSecurityReview: false,\n requiresJustification: false,\n },\n {\n key: 'crashes:read',\n label: 'Read crash data',\n description: 'Crash-free rate and issue counts',\n requiresSecurityReview: false,\n requiresJustification: false,\n },\n {\n key: 'revenue:read',\n label: 'Read revenue',\n description: 'MRR, revenue series and refund counts',\n requiresSecurityReview: false,\n requiresJustification: true,\n },\n {\n key: 'metrics:write',\n label: 'Write connector metrics',\n description:\n 'Write the metrics this extension declares into the metrics store',\n requiresSecurityReview: false,\n requiresJustification: false,\n },\n {\n key: 'alerts:write',\n label: 'Raise alerts and banners',\n description:\n 'Turn declared webhooks into alerts and banners, at most three a day per project',\n requiresSecurityReview: false,\n requiresJustification: true,\n },\n {\n key: 'insights:write',\n label: 'Post insights',\n description: 'Post insights to Home and to chat',\n requiresSecurityReview: false,\n requiresJustification: true,\n },\n {\n key: 'ai:tools',\n label: 'Add assistant tools',\n description: 'Offer tools and mentions to the assistant',\n requiresSecurityReview: false,\n requiresJustification: false,\n },\n {\n key: 'ai:actions',\n label: 'Run assistant actions',\n description: 'Run declared actions, each behind the approval card',\n requiresSecurityReview: true,\n requiresJustification: true,\n },\n {\n key: 'store.metadata:write',\n label: 'Propose store metadata changes',\n description: 'Propose listing changes through a Tappify approval',\n requiresSecurityReview: true,\n requiresJustification: true,\n },\n {\n key: 'autopilot:trigger',\n label: 'Trigger automated store actions',\n description: 'Start automated store actions on the owner’s behalf',\n requiresSecurityReview: true,\n requiresJustification: true,\n },\n {\n key: 'work:create',\n label: 'Create work items',\n description: 'Create issues, pages or messages in the vendor’s system',\n requiresSecurityReview: false,\n requiresJustification: true,\n },\n {\n key: 'work:sync',\n label: 'Sync work items',\n description: 'Two-way comments and status on created work items',\n requiresSecurityReview: false,\n requiresJustification: true,\n },\n {\n key: 'storage:write',\n label: 'Use hosted storage',\n description:\n 'Read and write this extension’s own hosted storage collections',\n requiresSecurityReview: false,\n requiresJustification: false,\n },\n {\n key: 'messaging:send',\n label: 'Send messages to end users',\n description:\n 'Send messages or push notifications to the owner’s end users through owner-supplied credentials',\n requiresSecurityReview: true,\n requiresJustification: true,\n },\n {\n key: 'ai:skills',\n label: 'Shape the assistant',\n description:\n 'Add skills, prompt templates and context providers that shape the assistant’s behaviour',\n requiresSecurityReview: false,\n requiresJustification: true,\n },\n] as const;\n\nexport type ScopeDefinition = (typeof SCOPES)[number];\n/** Every scope an extension can ask for under `scopes` in its manifest. */\nexport type ScopeKey = ScopeDefinition['key'];\n\nexport const SCOPE_KEYS: readonly ScopeKey[] = SCOPES.map(scope => scope.key);\n\nexport const SLOTS = [\n 'kpi-row',\n 'insights',\n 'sidebar',\n 'below-chart',\n 'below-list',\n] as const;\n/** The anchors on a host page a widget can be declared at. */\nexport type SlotId = (typeof SLOTS)[number];\n\nexport const CHARTS = [\n 'installs-activity',\n 'conversion',\n 'release-cadence',\n] as const;\nexport type HostChart = (typeof CHARTS)[number];\n\nexport const PAGES = [\n {\n key: 'overview',\n label: 'Home',\n route: '/projects/:id/overview',\n slots: ['kpi-row', 'insights', 'sidebar'],\n charts: ['installs-activity'],\n tabs: true,\n banners: true,\n },\n {\n key: 'analytics',\n label: 'Analytics',\n route: '/projects/:id/analytics',\n slots: ['kpi-row', 'below-chart', 'sidebar'],\n charts: ['installs-activity', 'conversion'],\n tabs: true,\n banners: true,\n },\n {\n key: 'deployments',\n label: 'Deployments',\n route: '/projects/:id/deployments',\n slots: ['sidebar', 'below-list'],\n charts: ['release-cadence'],\n tabs: true,\n banners: false,\n },\n {\n key: 'ai_chat',\n label: 'Assistant',\n route: 'chat',\n slots: [],\n charts: [],\n tabs: false,\n banners: false,\n },\n] as const;\n\nexport type PageDefinition = (typeof PAGES)[number];\n/** The host pages a contribution can attach a tab, widget or banner to. */\nexport type HostPage = PageDefinition['key'];\n\nexport const PAGE_KEYS: readonly HostPage[] = PAGES.map(page => page.key);\n\nexport const TABLES = [\n { key: 'analytics.keywords', label: 'Keywords' },\n { key: 'analytics.reviews', label: 'Reviews' },\n { key: 'deployments.releases', label: 'Releases' },\n { key: 'deployments.builds', label: 'Builds' },\n] as const;\n\nexport type TableDefinition = (typeof TABLES)[number];\nexport type HostTable = TableDefinition['key'];\n\nexport const TABLE_KEYS: readonly HostTable[] = TABLES.map(table => table.key);\n\nexport const KINDS = [\n 'page',\n 'tab',\n 'widget',\n 'series',\n 'banner',\n 'rowAction',\n 'marker',\n 'settings',\n 'storage',\n 'connector',\n 'webhook',\n 'tool',\n 'mention',\n 'action',\n 'knowledge',\n 'skill',\n 'prompt',\n 'context',\n 'work',\n] as const;\nexport type ContributionKind = (typeof KINDS)[number];\n\nexport const CATEGORIES = [\n 'market_data',\n 'subscriptions',\n 'product_analytics',\n 'crashes',\n 'attribution',\n 'experiments',\n 'messaging',\n 'session_replay',\n 'localization',\n 'design',\n 'ci_cd',\n 'trackers',\n 'support',\n 'chat',\n 'ad_monetization',\n 'warehouse',\n 'compliance',\n 'additional_stores',\n] as const;\nexport type ExtensionCategory = (typeof CATEGORIES)[number];\n\nexport const EVENTS = [\n 'release.shipped',\n 'metadata.changed',\n 'keyword.set_changed',\n 'screenshots.updated',\n 'price.changed',\n 'featuring.started',\n 'score.changed',\n 'incident.opened',\n 'incident.resolved',\n 'playbook.step_approved',\n 'review.thread_opened',\n 'review.thread_resolved',\n 'approval.requested',\n 'digest.sent',\n 'install.created',\n 'install.paused',\n 'install.resumed',\n 'install.revoked',\n 'install.token_rotated',\n 'scopes.changed',\n 'settings.changed',\n] as const;\n/** Every event Tappify emits, which a mount or a server can subscribe to. */\nexport type HostEventName = (typeof EVENTS)[number];\n\nexport const SERVED_SDK_MAJORS: readonly number[] = [2];\n\n/** The `kind` of every read `tap.data.query` accepts, one per scope it needs. */\nexport type DataQueryKind =\n | 'project'\n | 'series'\n | 'keywords'\n | 'listing'\n | 'reviews'\n | 'crashes'\n | 'revenue';\n\nexport const QUERY_SCOPES: Record<DataQueryKind, ScopeKey> = {\n project: 'projects:read',\n series: 'analytics:read',\n keywords: 'store.metadata:read',\n listing: 'store.metadata:read',\n reviews: 'reviews:read',\n crashes: 'crashes:read',\n revenue: 'revenue:read',\n};\n\n/**\n * The keys a tool's answer carries for each `returns` kind it may declare.\n * `validateToolResponse`, `tappify extension doctor` and the host all read\n * this one table, so a wrong body fails the same way wherever it is checked.\n */\nexport const CARD_KEYS: Record<ToolDeclaration['returns'], string[]> = {\n value: ['label', 'value'],\n series: ['label', 'points'],\n table: ['columns', 'rows'],\n list: ['items'],\n comparison: ['label', 'left', 'right'],\n};\n\nexport const findScope = (key: string): ScopeDefinition | undefined =>\n SCOPES.find(scope => scope.key === key);\n\nexport const findPage = (key: string): PageDefinition | undefined =>\n PAGES.find(page => page.key === key);\n\nexport const findTable = (key: string): TableDefinition | undefined =>\n TABLES.find(table => table.key === key);\n\nexport const slotsForPage = (page: string): readonly SlotId[] =>\n findPage(page)?.slots ?? [];\n"],"mappings":"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","names":[]}