@tangle-network/agent-app 0.45.47 → 0.45.49
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{DesignCanvas-P2ZVK6FM.js → DesignCanvas-JRBCAWHW.js} +2 -2
- package/dist/{DesignCanvasEditor-DGNCA54P.js → DesignCanvasEditor-J2OLS22G.js} +4 -4
- package/dist/{IntakeInterview-3ZIQMZS6.js → IntakeInterview-OKT6DCBN.js} +2 -2
- package/dist/InvitationsPanel-OFCYVSAE.js +9 -0
- package/dist/InviteAcceptPage-BDOO4JSE.js +7 -0
- package/dist/MembersPanel-3ZK2GCMC.js +9 -0
- package/dist/{TimelineEditor-OTNPJ3SN.js → TimelineEditor-52YEPDX5.js} +2 -2
- package/dist/VaultPane-IGJKB6CX.js +7 -0
- package/dist/assistant/index.d.ts +36 -11
- package/dist/assistant/index.js +235 -90
- package/dist/assistant/index.js.map +1 -1
- package/dist/{billing-BibxgALe.d.ts → billing-BuNlaKlE.d.ts} +15 -23
- package/dist/chat-react/index.js +2 -1
- package/dist/chat-react/index.js.map +1 -1
- package/dist/{chunk-TL37E5YY.js → chunk-33BN7P57.js} +6 -6
- package/dist/chunk-33BN7P57.js.map +1 -0
- package/dist/{chunk-IUSO2XRE.js → chunk-5MI2SKQH.js} +14 -15
- package/dist/chunk-5MI2SKQH.js.map +1 -0
- package/dist/{chunk-65OZT566.js → chunk-7LJYCHXK.js} +29 -27
- package/dist/chunk-7LJYCHXK.js.map +1 -0
- package/dist/chunk-AULM4X7U.js +77 -0
- package/dist/chunk-AULM4X7U.js.map +1 -0
- package/dist/{chunk-GNPN6ED4.js → chunk-CJHIAZKK.js} +128 -166
- package/dist/chunk-CJHIAZKK.js.map +1 -0
- package/dist/{chunk-54FYYXAX.js → chunk-ENLRJYVW.js} +21 -18
- package/dist/chunk-ENLRJYVW.js.map +1 -0
- package/dist/{chunk-F4XLTVQ3.js → chunk-GRDLGQ6A.js} +19 -18
- package/dist/chunk-GRDLGQ6A.js.map +1 -0
- package/dist/{chunk-BT4QFCTB.js → chunk-HSBJB46C.js} +13 -1
- package/dist/chunk-HSBJB46C.js.map +1 -0
- package/dist/{chunk-VOJB7MVG.js → chunk-I2CA5N7V.js} +70 -28
- package/dist/chunk-I2CA5N7V.js.map +1 -0
- package/dist/chunk-MLG6XKPV.js +44 -0
- package/dist/chunk-MLG6XKPV.js.map +1 -0
- package/dist/{chunk-IN6LNZ7O.js → chunk-Q2QKEV6J.js} +7 -7
- package/dist/{chunk-IN6LNZ7O.js.map → chunk-Q2QKEV6J.js.map} +1 -1
- package/dist/{chunk-B62EZF52.js → chunk-UMV45ZDF.js} +3 -3
- package/dist/{chunk-JJJEO4ND.js → chunk-VWTODXRW.js} +2 -2
- package/dist/{chunk-JJJEO4ND.js.map → chunk-VWTODXRW.js.map} +1 -1
- package/dist/{chunk-VCPZ3HTN.js → chunk-W7P5KV6O.js} +51 -27
- package/dist/chunk-W7P5KV6O.js.map +1 -0
- package/dist/{chunk-ZX5UT4HF.js → chunk-ZAKKG6WI.js} +354 -273
- package/dist/chunk-ZAKKG6WI.js.map +1 -0
- package/dist/{chunk-PE67O2LH.js → chunk-ZGY6QB4F.js} +21 -16
- package/dist/chunk-ZGY6QB4F.js.map +1 -0
- package/dist/design-canvas-react/index.js +18 -16
- package/dist/design-canvas-react/index.js.map +1 -1
- package/dist/design-canvas-react/lazy.js +1 -1
- package/dist/intakes-react/index.js +1 -1
- package/dist/intakes-react/lazy.js +1 -1
- package/dist/platform/index.d.ts +1 -1
- package/dist/platform/index.js +16 -16
- package/dist/platform/index.js.map +1 -1
- package/dist/sequences-react/index.js +2 -2
- package/dist/studio/index.d.ts +13 -1
- package/dist/studio/index.js +13 -1
- package/dist/studio-react/index.js +115 -78
- package/dist/studio-react/index.js.map +1 -1
- package/dist/studio-react/studio.css +0 -56
- package/dist/teams-react/index.js +4 -3
- package/dist/teams-react/lazy.js +3 -3
- package/dist/theme/index.js +1 -1
- package/dist/theme/tokens.css +15 -13
- package/dist/vault/index.js +1 -1
- package/dist/vault/lazy.js +1 -1
- package/dist/web-react/index.d.ts +16 -12
- package/dist/web-react/index.js +6 -4
- package/dist/work-product-react/index.js +9 -27
- package/dist/work-product-react/index.js.map +1 -1
- package/package.json +3 -3
- package/dist/InvitationsPanel-F56UOQXA.js +0 -8
- package/dist/InviteAcceptPage-P3QJC3C6.js +0 -7
- package/dist/MembersPanel-XNLF3SA4.js +0 -8
- package/dist/VaultPane-MUWGYVJ7.js +0 -7
- package/dist/chunk-54FYYXAX.js.map +0 -1
- package/dist/chunk-65OZT566.js.map +0 -1
- package/dist/chunk-BT4QFCTB.js.map +0 -1
- package/dist/chunk-F4XLTVQ3.js.map +0 -1
- package/dist/chunk-GNPN6ED4.js.map +0 -1
- package/dist/chunk-IUSO2XRE.js.map +0 -1
- package/dist/chunk-PE67O2LH.js.map +0 -1
- package/dist/chunk-TL37E5YY.js.map +0 -1
- package/dist/chunk-VCPZ3HTN.js.map +0 -1
- package/dist/chunk-VOJB7MVG.js.map +0 -1
- package/dist/chunk-ZX5UT4HF.js.map +0 -1
- /package/dist/{DesignCanvas-P2ZVK6FM.js.map → DesignCanvas-JRBCAWHW.js.map} +0 -0
- /package/dist/{DesignCanvasEditor-DGNCA54P.js.map → DesignCanvasEditor-J2OLS22G.js.map} +0 -0
- /package/dist/{IntakeInterview-3ZIQMZS6.js.map → IntakeInterview-OKT6DCBN.js.map} +0 -0
- /package/dist/{InvitationsPanel-F56UOQXA.js.map → InvitationsPanel-OFCYVSAE.js.map} +0 -0
- /package/dist/{InviteAcceptPage-P3QJC3C6.js.map → InviteAcceptPage-BDOO4JSE.js.map} +0 -0
- /package/dist/{MembersPanel-XNLF3SA4.js.map → MembersPanel-3ZK2GCMC.js.map} +0 -0
- /package/dist/{TimelineEditor-OTNPJ3SN.js.map → TimelineEditor-52YEPDX5.js.map} +0 -0
- /package/dist/{VaultPane-MUWGYVJ7.js.map → VaultPane-IGJKB6CX.js.map} +0 -0
- /package/dist/{chunk-B62EZF52.js.map → chunk-UMV45ZDF.js.map} +0 -0
|
@@ -12,7 +12,7 @@ import {
|
|
|
12
12
|
downloadDataUrl,
|
|
13
13
|
exportDocumentJson,
|
|
14
14
|
exportPageDataUrl
|
|
15
|
-
} from "../chunk-
|
|
15
|
+
} from "../chunk-33BN7P57.js";
|
|
16
16
|
import {
|
|
17
17
|
BTN,
|
|
18
18
|
BTN_ACTIVE,
|
|
@@ -41,8 +41,8 @@ import {
|
|
|
41
41
|
buildRulerTicks,
|
|
42
42
|
formatRulerLabel,
|
|
43
43
|
selectTickStep
|
|
44
|
-
} from "../chunk-
|
|
45
|
-
import "../chunk-
|
|
44
|
+
} from "../chunk-7LJYCHXK.js";
|
|
45
|
+
import "../chunk-Q2QKEV6J.js";
|
|
46
46
|
import "../chunk-ZN5J47UX.js";
|
|
47
47
|
import {
|
|
48
48
|
DEFAULT_INSERT_TEMPLATES,
|
|
@@ -93,7 +93,7 @@ import {
|
|
|
93
93
|
import {
|
|
94
94
|
DesignCanvasChromeLazy,
|
|
95
95
|
DesignCanvasLazy
|
|
96
|
-
} from "../chunk-
|
|
96
|
+
} from "../chunk-UMV45ZDF.js";
|
|
97
97
|
import "../chunk-F7VH4G5C.js";
|
|
98
98
|
|
|
99
99
|
// src/design-canvas-react/components/CanvasInsertPanel.tsx
|
|
@@ -170,11 +170,13 @@ function CanvasInsertPanel({
|
|
|
170
170
|
onInsert,
|
|
171
171
|
onUploadImage,
|
|
172
172
|
loadGenerations,
|
|
173
|
-
templates
|
|
173
|
+
templates,
|
|
174
174
|
accept = DEFAULT_ACCEPT,
|
|
175
175
|
className
|
|
176
176
|
}) {
|
|
177
|
-
const
|
|
177
|
+
const resolvedTemplates = templates ?? DEFAULT_INSERT_TEMPLATES;
|
|
178
|
+
const templatesLabel = templates !== void 0 ? "Templates" : "Elements";
|
|
179
|
+
const [tab, setTab] = useState(() => resolvedTemplates.length > 0 ? "templates" : "uploads");
|
|
178
180
|
const [busy, setBusy] = useState(false);
|
|
179
181
|
const [error, setError] = useState("");
|
|
180
182
|
const [dragOver, setDragOver] = useState(false);
|
|
@@ -183,15 +185,15 @@ function CanvasInsertPanel({
|
|
|
183
185
|
const fileInputRef = useRef(null);
|
|
184
186
|
useEffect(() => {
|
|
185
187
|
if (tab === "uploads" && !onUploadImage) {
|
|
186
|
-
setTab(
|
|
188
|
+
setTab(resolvedTemplates.length > 0 ? "templates" : loadGenerations ? "generations" : "uploads");
|
|
187
189
|
}
|
|
188
|
-
if (tab === "templates" &&
|
|
190
|
+
if (tab === "templates" && resolvedTemplates.length === 0) {
|
|
189
191
|
setTab(onUploadImage ? "uploads" : loadGenerations ? "generations" : "uploads");
|
|
190
192
|
}
|
|
191
193
|
if (tab === "generations" && !loadGenerations) {
|
|
192
|
-
setTab(
|
|
194
|
+
setTab(resolvedTemplates.length > 0 ? "templates" : onUploadImage ? "uploads" : "templates");
|
|
193
195
|
}
|
|
194
|
-
}, [tab,
|
|
196
|
+
}, [tab, resolvedTemplates.length, onUploadImage, loadGenerations]);
|
|
195
197
|
useEffect(() => {
|
|
196
198
|
if (tab !== "generations" || generationsLoaded || !loadGenerations) return;
|
|
197
199
|
let cancelled = false;
|
|
@@ -249,7 +251,7 @@ function CanvasInsertPanel({
|
|
|
249
251
|
}
|
|
250
252
|
const tabs = [
|
|
251
253
|
{ id: "uploads", label: "Uploads", icon: ImageGlyph, show: !!onUploadImage },
|
|
252
|
-
{ id: "templates", label:
|
|
254
|
+
{ id: "templates", label: templatesLabel, icon: ShapesGlyph, show: resolvedTemplates.length > 0 },
|
|
253
255
|
{ id: "generations", label: "Generations", icon: SparkleGlyph, show: !!loadGenerations }
|
|
254
256
|
];
|
|
255
257
|
const visibleTabs = tabs.filter((t) => t.show);
|
|
@@ -308,7 +310,7 @@ function CanvasInsertPanel({
|
|
|
308
310
|
}
|
|
309
311
|
)
|
|
310
312
|
] }),
|
|
311
|
-
tab === "templates" && /* @__PURE__ */ jsx("div", { className: "grid grid-cols-2 gap-2", children:
|
|
313
|
+
tab === "templates" && /* @__PURE__ */ jsx("div", { className: "grid grid-cols-2 gap-2", children: resolvedTemplates.map((tpl) => /* @__PURE__ */ jsxs(
|
|
312
314
|
"button",
|
|
313
315
|
{
|
|
314
316
|
type: "button",
|
|
@@ -409,9 +411,9 @@ function LayersPanel({ page, selectedElementIds, canWrite, onSetAttrs, onReorder
|
|
|
409
411
|
}
|
|
410
412
|
const visible = rows.length > LAYERS_PANEL_ROW_LIMIT ? rows.slice(0, LAYERS_PANEL_ROW_LIMIT) : rows;
|
|
411
413
|
return /* @__PURE__ */ jsxs2("div", { className: "flex h-full flex-col overflow-hidden text-[var(--text-primary)]", children: [
|
|
412
|
-
/* @__PURE__ */ jsx2("div", { className: "shrink-0 border-b border-[var(--border-default)] px-3 py-1.5 text-
|
|
414
|
+
/* @__PURE__ */ jsx2("div", { className: "shrink-0 border-b border-[var(--border-default)] px-3 py-1.5 text-xs font-semibold uppercase tracking-[0.05em] text-[var(--text-muted)]", children: "Layers" }),
|
|
413
415
|
/* @__PURE__ */ jsxs2("div", { className: "min-h-0 flex-1 overflow-y-auto", children: [
|
|
414
|
-
visible.length === 0 ? /* @__PURE__ */ jsx2("div", { className: "px-3 py-2 text-
|
|
416
|
+
visible.length === 0 ? /* @__PURE__ */ jsx2("div", { className: "px-3 py-2 text-xs text-[var(--text-muted)]", children: "No layers yet" }) : null,
|
|
415
417
|
visible.map((row) => {
|
|
416
418
|
const { element } = row;
|
|
417
419
|
const isSelected = selectedElementIds.includes(element.id);
|
|
@@ -484,7 +486,7 @@ function LayersPanel({ page, selectedElementIds, canWrite, onSetAttrs, onReorder
|
|
|
484
486
|
className: "flex min-w-0 flex-1 cursor-pointer items-center gap-1.5 bg-transparent text-left",
|
|
485
487
|
children: [
|
|
486
488
|
/* @__PURE__ */ jsx2(KindIcon, { kind: element.kind, className: "h-3.5 w-3.5 shrink-0 opacity-60" }),
|
|
487
|
-
element.slot ? /* @__PURE__ */ jsx2(SlotGlyph, { className: "h-3 w-3
|
|
489
|
+
element.slot ? /* @__PURE__ */ jsx2("span", { title: `Bound to slot: ${element.slot}`, className: "inline-flex shrink-0", children: /* @__PURE__ */ jsx2(SlotGlyph, { className: "h-3 w-3 text-[var(--brand-primary)]" }) }) : null,
|
|
488
490
|
/* @__PURE__ */ jsx2("span", { className: "min-w-0 flex-1 truncate", children: element.name })
|
|
489
491
|
]
|
|
490
492
|
}
|
|
@@ -531,7 +533,7 @@ function LayersPanel({ page, selectedElementIds, canWrite, onSetAttrs, onReorder
|
|
|
531
533
|
element.id
|
|
532
534
|
);
|
|
533
535
|
}),
|
|
534
|
-
rows.length > LAYERS_PANEL_ROW_LIMIT ? /* @__PURE__ */ jsxs2("div", { className: "px-3 py-2 text-
|
|
536
|
+
rows.length > LAYERS_PANEL_ROW_LIMIT ? /* @__PURE__ */ jsxs2("div", { className: "px-3 py-2 text-xs text-[var(--text-muted)]", children: [
|
|
535
537
|
"+",
|
|
536
538
|
rows.length - LAYERS_PANEL_ROW_LIMIT,
|
|
537
539
|
" more elements \u2014 select a group to scope the list"
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/design-canvas-react/components/CanvasInsertPanel.tsx","../../src/design-canvas-react/components/LayersPanel.tsx","../../src/design-canvas-react/components/layer-tree.ts"],"sourcesContent":["/**\n * The human \"add element to the canvas\" side panel — a reusable left rail any\n * canvas consumer can mount so a person (not just the agent) can put images,\n * shapes, and text on the canvas. Designed for the `renderSidePanel` slot of\n * {@link DesignCanvasProps}; pass `renderSidePanel={(ctx) => <CanvasInsertPanel … />}`.\n *\n * Endpoints are per-app, so every I/O path is a callback — no product route is\n * hardcoded:\n * - `onUploadImage(file) => Promise<url>` — optional. When provided, the host\n * stores the file and returns the src to insert. The url MUST be http(s) or\n * a rooted `/api/` path (enforced by `assertSceneMediaSrc` before insertion);\n * a `data:` url is rejected, matching the scene model's media boundary.\n * - `loadGenerations?()` — optional provider for \"already generated in this\n * workspace\" images; omit to hide the tab.\n * - `templates?` — optional template set; defaults to {@link DEFAULT_INSERT_TEMPLATES}.\n *\n * Insertion goes through `onInsert`, which the host wires to its\n * `onApplyOperations` pipeline (server-validated, undoable) — the same path\n * every other edit takes.\n *\n * Tokens/icons follow the canvas convention: CSS-var design tokens and inline\n * SVG glyphs, no icon-library or Tailwind-semantic-token dependency.\n */\n\nimport { useEffect, useRef, useState, type ReactElement } from 'react'\nimport type { SceneOperation } from '../../design-canvas/operations'\nimport { assertSceneMediaSrc } from '../../design-canvas/model'\nimport {\n DEFAULT_INSERT_TEMPLATES,\n buildInsertImageOp,\n type InsertPageGeometry,\n type InsertTemplate,\n} from '../insert-builders'\nimport { ImageGlyph, ShapesGlyph } from './glyphs'\n\n/** An already-generated image the panel can offer for one-click insertion. */\nexport interface InsertGeneration {\n id: string\n /** The image url to insert. Must satisfy the scene media boundary\n * (http(s) or rooted `/api/`); rejected otherwise at insert time. */\n url: string\n /** Optional prompt/label shown as the tile's title. */\n label?: string\n}\n\nexport interface CanvasInsertPanelProps {\n canWrite: boolean\n /** The active page new elements are added to. */\n page: InsertPageGeometry\n /** Submit operations through the host's apply pipeline. */\n onInsert(operations: SceneOperation[]): Promise<unknown>\n /** Store an uploaded file and return its src (http(s) or rooted `/api/`). */\n onUploadImage?(file: File): Promise<string>\n /** Optional provider for the Generations tab; omit to hide it. */\n loadGenerations?(): Promise<InsertGeneration[]>\n /** Drop-in templates; defaults to the built-in starter set. */\n templates?: readonly InsertTemplate[]\n /** Accepted upload mime types. Default: PNG/JPEG/GIF/WebP. */\n accept?: string\n className?: string\n}\n\ntype Tab = 'uploads' | 'templates' | 'generations'\n\nconst DEFAULT_ACCEPT = 'image/png,image/jpeg,image/gif,image/webp'\n\n/** Read an image url's natural dimensions in the browser; resolves `{0,0}` when\n * Image is unavailable (SSR/tests), the load fails, or the probe times out —\n * builders fall back to the size cap, so a failed probe still inserts a sensibly\n * sized element rather than blocking insertion. */\nconst PROBE_TIMEOUT_MS = 4000\n\nfunction probeImageSize(url: string): Promise<{ width: number; height: number }> {\n return new Promise((resolve) => {\n if (typeof Image === 'undefined') return resolve({ width: 0, height: 0 })\n let settled = false\n const finish = (size: { width: number; height: number }) => {\n if (settled) return\n settled = true\n resolve(size)\n }\n const img = new Image()\n img.onload = () => finish({ width: img.naturalWidth, height: img.naturalHeight })\n img.onerror = () => finish({ width: 0, height: 0 })\n // A URL that never loads (or an environment that never fires load/error)\n // must not hang the insert forever.\n setTimeout(() => finish({ width: 0, height: 0 }), PROBE_TIMEOUT_MS)\n img.src = url\n })\n}\n\nfunction UploadGlyph({ className }: { className?: string }) {\n return (\n <svg className={className} viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\" strokeLinecap=\"round\" strokeLinejoin=\"round\" aria-hidden>\n <path d=\"M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4M7 9l5-5 5 5M12 4v12\" />\n </svg>\n )\n}\n\nfunction SparkleGlyph({ className }: { className?: string }) {\n return (\n <svg className={className} viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\" strokeLinecap=\"round\" strokeLinejoin=\"round\" aria-hidden>\n <path d=\"M12 3v3m0 12v3M3 12h3m12 0h3M5.6 5.6l2.1 2.1m8.6 8.6 2.1 2.1m0-12.8-2.1 2.1M7.7 16.3l-2.1 2.1\" />\n </svg>\n )\n}\n\nfunction SpinnerGlyph({ className }: { className?: string }) {\n return (\n <svg className={`${className ?? ''} animate-spin`} viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\" aria-hidden>\n <path d=\"M21 12a9 9 0 1 1-6.2-8.6\" strokeLinecap=\"round\" />\n </svg>\n )\n}\n\n/** Probe-page geometry for deriving a template's primary element kind. The\n * result drives only the tile preview, so the exact dimensions don't matter. */\nconst PREVIEW_PROBE_PAGE: InsertPageGeometry = { pageId: 'preview', width: 1000, height: 1000 }\n\n/** The visual a template tile shows. Derived from the first element the\n * template's (pure) `build` produces, so custom templates render correctly\n * too — not just the built-in set. */\ntype TemplateShape = 'heading' | 'body' | 'rect' | 'ellipse' | 'other'\n\n/** A text element is treated as a heading (shown as \"T\") when it reads as a\n * title — bold or set in a large face; otherwise body copy (shown as \"¶\"). */\nconst HEADING_FONT_SIZE = 32\n\nfunction templateShape(tpl: InsertTemplate): TemplateShape {\n try {\n const ops = tpl.build(PREVIEW_PROBE_PAGE)\n const added = ops.find((op) => op.type === 'add_element')\n if (!added || added.type !== 'add_element') return 'other'\n const el = added.element\n if (el.kind === 'rect') return 'rect'\n if (el.kind === 'ellipse') return 'ellipse'\n if (el.kind === 'text') {\n const isHeading = el.fontStyle.includes('bold') || el.fontSize >= HEADING_FONT_SIZE\n return isHeading ? 'heading' : 'body'\n }\n return 'other'\n } catch {\n // A throwing build can't preview; fall through to the neutral shape glyph.\n return 'other'\n }\n}\n\n/** A small visual standing in for what the template inserts, so tiles are\n * distinguishable at a glance instead of four identical text chips. */\nfunction TemplatePreview({ shape }: { shape: TemplateShape }) {\n if (shape === 'rect') {\n return <span className=\"block h-6 w-9 rounded-sm bg-[var(--brand-primary)]\" aria-hidden />\n }\n if (shape === 'ellipse') {\n return <span className=\"block h-7 w-7 rounded-full bg-[var(--brand-primary)]\" aria-hidden />\n }\n // Text tiles show a glyph: a semibold \"T\" for a heading, a paragraph mark\n // \"¶\" for body copy.\n if (shape === 'heading' || shape === 'body') {\n return (\n <span\n className={`block leading-none text-[var(--text-primary)] ${\n shape === 'heading' ? 'text-2xl font-semibold' : 'text-lg'\n }`}\n aria-hidden\n >\n {shape === 'heading' ? 'T' : '¶'}\n </span>\n )\n }\n return <ShapesGlyph className=\"h-6 w-6 text-[var(--text-muted)]\" />\n}\n\nexport function CanvasInsertPanel({\n canWrite,\n page,\n onInsert,\n onUploadImage,\n loadGenerations,\n templates = DEFAULT_INSERT_TEMPLATES,\n accept = DEFAULT_ACCEPT,\n className,\n}: CanvasInsertPanelProps) {\n const [tab, setTab] = useState<Tab>(() => (templates.length > 0 ? 'templates' : 'uploads'))\n const [busy, setBusy] = useState(false)\n const [error, setError] = useState('')\n const [dragOver, setDragOver] = useState(false)\n const [generations, setGenerations] = useState<InsertGeneration[]>([])\n const [generationsLoaded, setGenerationsLoaded] = useState(false)\n const fileInputRef = useRef<HTMLInputElement | null>(null)\n\n useEffect(() => {\n if (tab === 'uploads' && !onUploadImage) {\n setTab(templates.length > 0 ? 'templates' : loadGenerations ? 'generations' : 'uploads')\n }\n if (tab === 'templates' && templates.length === 0) {\n setTab(onUploadImage ? 'uploads' : loadGenerations ? 'generations' : 'uploads')\n }\n if (tab === 'generations' && !loadGenerations) {\n setTab(templates.length > 0 ? 'templates' : onUploadImage ? 'uploads' : 'templates')\n }\n }, [tab, templates.length, onUploadImage, loadGenerations])\n\n useEffect(() => {\n if (tab !== 'generations' || generationsLoaded || !loadGenerations) return\n let cancelled = false\n void (async () => {\n try {\n const rows = await loadGenerations()\n if (!cancelled) setGenerations(rows.filter((g) => g.url))\n } catch (err) {\n if (!cancelled) setError(err instanceof Error ? err.message : 'Could not load generations')\n } finally {\n if (!cancelled) setGenerationsLoaded(true)\n }\n })()\n return () => {\n cancelled = true\n }\n }, [tab, generationsLoaded, loadGenerations])\n\n async function insertImageFromUrl(url: string) {\n // Validate the media boundary BEFORE probing — a data: or sandbox-local url\n // throws here (surfaced as an inline error) without a wasted image load.\n assertSceneMediaSrc(url, 'image src')\n const natural = await probeImageSize(url)\n await onInsert([buildInsertImageOp(url, natural, page)])\n }\n\n async function handleFiles(files: FileList | File[]) {\n if (!canWrite || busy) return\n if (!onUploadImage) return\n const list = Array.from(files).filter((f) => f.type.startsWith('image/'))\n if (list.length === 0) {\n setError('Only image files can be added to the canvas')\n return\n }\n setBusy(true)\n setError('')\n try {\n for (const file of list) {\n const url = await onUploadImage(file)\n await insertImageFromUrl(url)\n }\n } catch (err) {\n setError(err instanceof Error ? err.message : 'Upload failed')\n } finally {\n setBusy(false)\n }\n }\n\n async function runInsert(build: () => SceneOperation[] | Promise<SceneOperation[]>) {\n if (!canWrite || busy) return\n setBusy(true)\n setError('')\n try {\n await onInsert(await build())\n } catch (err) {\n setError(err instanceof Error ? err.message : 'Insert failed')\n } finally {\n setBusy(false)\n }\n }\n\n const tabs: Array<{ id: Tab; label: string; icon: (p: { className?: string }) => ReactElement; show: boolean }> = [\n { id: 'uploads', label: 'Uploads', icon: ImageGlyph, show: !!onUploadImage },\n { id: 'templates', label: 'Templates', icon: ShapesGlyph, show: templates.length > 0 },\n { id: 'generations', label: 'Generations', icon: SparkleGlyph, show: !!loadGenerations },\n ]\n const visibleTabs = tabs.filter((t) => t.show)\n\n return (\n <div className={`flex h-full min-h-0 flex-col bg-[var(--bg-input)] text-[var(--text-primary)] ${className ?? ''}`}>\n <div className=\"flex shrink-0 border-b border-[var(--border-default)]\">\n {visibleTabs.map(({ id, label, icon: Icon }) => (\n <button\n key={id}\n type=\"button\"\n onClick={() => setTab(id)}\n // These tabs sit flush against the top and side edges of a panel\n // that clips its overflow, so the floor's outward ring is cut off on\n // three sides and reads as a stray underline. The negative offset\n // draws the same ring inside the tab; width and colour still come\n // from the tokens.\n className={`flex flex-1 items-center justify-center gap-1.5 px-2 py-2.5 text-xs font-medium transition-colors focus-visible:[outline-offset:-2px] ${\n tab === id\n ? 'border-b-2 border-[var(--brand-primary)] text-[var(--text-primary)]'\n : 'text-[var(--text-muted)] hover:text-[var(--text-primary)]'\n }`}\n >\n <Icon className=\"h-3.5 w-3.5\" />\n {label}\n </button>\n ))}\n </div>\n\n {!canWrite ? (\n <div className=\"flex flex-1 items-center justify-center p-4 text-center text-sm text-[var(--text-muted)]\">\n You have view-only access to this design.\n </div>\n ) : (\n <div className=\"min-h-0 flex-1 overflow-y-auto p-3\">\n {tab === 'uploads' && (\n <div className=\"flex flex-col gap-3\">\n <button\n type=\"button\"\n disabled={busy}\n onClick={() => fileInputRef.current?.click()}\n onDragOver={(e) => {\n e.preventDefault()\n setDragOver(true)\n }}\n onDragLeave={() => setDragOver(false)}\n onDrop={(e) => {\n e.preventDefault()\n setDragOver(false)\n if (e.dataTransfer?.files?.length) void handleFiles(e.dataTransfer.files)\n }}\n className={`flex flex-col items-center justify-center gap-2 rounded-lg border-2 border-dashed px-4 py-8 text-center transition-colors disabled:opacity-50 ${\n dragOver\n ? 'border-[var(--brand-primary)] bg-[color-mix(in_srgb,var(--brand-primary)_5%,transparent)]'\n : 'border-[var(--border-default)] hover:border-[color-mix(in_srgb,var(--brand-primary)_40%,transparent)]'\n }`}\n >\n {busy ? (\n <SpinnerGlyph className=\"h-6 w-6 text-[var(--brand-primary)]\" />\n ) : (\n <UploadGlyph className=\"h-6 w-6 text-[var(--text-muted)]\" />\n )}\n <span className=\"text-sm font-medium text-[var(--text-primary)]\">\n {busy ? 'Uploading…' : 'Drop an image or click to upload'}\n </span>\n <span className=\"text-xs text-[var(--text-muted)]\">PNG, JPEG, GIF, or WebP</span>\n </button>\n <input\n ref={fileInputRef}\n type=\"file\"\n accept={accept}\n multiple\n className=\"hidden\"\n onChange={(e) => {\n if (e.target.files?.length) void handleFiles(e.target.files)\n e.target.value = ''\n }}\n />\n </div>\n )}\n\n {tab === 'templates' && (\n <div className=\"grid grid-cols-2 gap-2\">\n {templates.map((tpl) => (\n <button\n key={tpl.id}\n type=\"button\"\n disabled={busy}\n onClick={() => void runInsert(() => tpl.build(page))}\n className=\"flex h-20 flex-col items-center justify-center gap-1.5 rounded-md border border-[var(--border-default)] bg-[var(--bg-input)] text-xs font-medium text-[var(--text-primary)] transition-colors hover:border-[color-mix(in_srgb,var(--brand-primary)_40%,transparent)] disabled:opacity-50\"\n >\n <span className=\"flex h-7 items-center justify-center\">\n <TemplatePreview shape={templateShape(tpl)} />\n </span>\n <span>{tpl.label}</span>\n </button>\n ))}\n </div>\n )}\n\n {tab === 'generations' && (\n <div className=\"flex flex-col gap-2\">\n {!generationsLoaded ? (\n <div className=\"flex items-center justify-center py-8 text-[var(--text-muted)]\">\n <SpinnerGlyph className=\"h-5 w-5\" />\n </div>\n ) : generations.length === 0 ? (\n <p className=\"px-1 py-6 text-center text-sm text-[var(--text-muted)]\">\n No generated images yet. Ask the agent to generate one.\n </p>\n ) : (\n <div className=\"grid grid-cols-2 gap-2\">\n {generations.map((gen) => (\n <button\n key={gen.id}\n type=\"button\"\n disabled={busy}\n onClick={() => {\n if (busy || !canWrite) return\n setBusy(true)\n setError('')\n insertImageFromUrl(gen.url)\n .catch((err) => setError(err instanceof Error ? err.message : 'Insert failed'))\n .finally(() => setBusy(false))\n }}\n title={gen.label}\n className=\"group relative aspect-square overflow-hidden rounded-md border border-[var(--border-default)] bg-[var(--bg-input)] transition-colors hover:border-[var(--brand-primary)] disabled:opacity-50\"\n >\n {/* eslint-disable-next-line jsx-a11y/img-redundant-alt */}\n <img src={gen.url} alt={gen.label || 'Generated image'} className=\"h-full w-full object-cover\" />\n </button>\n ))}\n </div>\n )}\n </div>\n )}\n\n {error ? <p className=\"mt-3 text-xs leading-5 text-[var(--text-danger,#dc2626)]\">{error}</p> : null}\n </div>\n )}\n </div>\n )\n}\n","/**\n * Layers panel — reverse-z list of the active page's elements. Highest z-index\n * is at the top. Groups show their children indented beneath them. Clicking\n * selects an element; meta-click adds to selection. Double-clicking the name\n * opens an inline rename input. Eye and lock icons toggle visibility/locked.\n * Rows can be drag-reordered within the list (siblings only; cross-group reorder\n * is a pending integrator concern). Capped at LAYERS_PANEL_ROW_LIMIT rows.\n */\n\nimport { useCallback, useMemo, useRef, useState } from 'react'\nimport type { SceneElement, ScenePage } from '../../design-canvas/model'\nimport { flattenLayerTree, LAYERS_PANEL_ROW_LIMIT } from './layer-tree'\nimport {\n EllipseGlyph,\n EyeGlyph,\n EyeOffGlyph,\n GroupGlyph,\n ImageGlyph,\n LineGlyph,\n LockGlyph,\n RectGlyph,\n SlotGlyph,\n TextGlyph,\n UnlockGlyph,\n VideoGlyph,\n} from './glyphs'\nimport type { SceneElementKind } from '../../design-canvas/model'\n\nfunction KindIcon({ kind, className }: { kind: SceneElementKind; className?: string }) {\n switch (kind) {\n case 'rect': return <RectGlyph className={className} />\n case 'ellipse': return <EllipseGlyph className={className} />\n case 'line': return <LineGlyph className={className} />\n case 'text': return <TextGlyph className={className} />\n case 'image': return <ImageGlyph className={className} />\n case 'video': return <VideoGlyph className={className} />\n case 'group': return <GroupGlyph className={className} />\n }\n}\n\nexport interface LayersPanelProps {\n page: ScenePage\n selectedElementIds: string[]\n canWrite: boolean\n /** Emit a set_attrs command for the given element. */\n onSetAttrs(elementId: string, attrs: Partial<Pick<SceneElement, 'name' | 'visible' | 'locked'>>): void\n /** Emit a reorder_element command. */\n onReorder(elementId: string, toIndex: number): void\n onSelect(elementId: string, additive: boolean): void\n}\n\nconst INDENT_PX = 16\n\nexport function LayersPanel({ page, selectedElementIds, canWrite, onSetAttrs, onReorder, onSelect }: LayersPanelProps) {\n const rows = useMemo(() => flattenLayerTree(page), [page])\n const [renamingId, setRenamingId] = useState<string | null>(null)\n const [renameValue, setRenameValue] = useState('')\n\n // Drag-reorder state: we track the dragged row's ownerIndex and the target\n // index within the SAME owner. Cross-owner reorder is not supported here.\n const dragRowRef = useRef<{ elementId: string; ownerIndex: number; ownerLength: number } | null>(null)\n const [dragOverIndex, setDragOverIndex] = useState<number | null>(null)\n\n function startRename(element: SceneElement) {\n setRenamingId(element.id)\n setRenameValue(element.name)\n }\n\n function commitRename(elementId: string) {\n const name = renameValue.trim()\n if (name.length > 0) onSetAttrs(elementId, { name })\n setRenamingId(null)\n }\n\n const visible = rows.length > LAYERS_PANEL_ROW_LIMIT ? rows.slice(0, LAYERS_PANEL_ROW_LIMIT) : rows\n\n return (\n <div className=\"flex h-full flex-col overflow-hidden text-[var(--text-primary)]\">\n <div className=\"shrink-0 border-b border-[var(--border-default)] px-3 py-1.5 text-[11px] font-semibold uppercase tracking-[0.05em] text-[var(--text-muted)]\">\n Layers\n </div>\n\n <div className=\"min-h-0 flex-1 overflow-y-auto\">\n {visible.length === 0 ? (\n <div className=\"px-3 py-2 text-[11px] text-[var(--text-muted)]\">No layers yet</div>\n ) : null}\n {visible.map((row) => {\n const { element } = row\n const isSelected = selectedElementIds.includes(element.id)\n const isRenaming = renamingId === element.id\n\n return (\n <div\n key={element.id}\n data-layer-row={element.id}\n draggable={canWrite}\n onDragStart={() => {\n dragRowRef.current = {\n elementId: element.id,\n ownerIndex: row.ownerIndex,\n ownerLength: row.ownerLength,\n }\n }}\n onDragOver={(event) => {\n if (!dragRowRef.current) return\n // Only allow reorder within the same owner (same parentGroupId + depth)\n event.preventDefault()\n setDragOverIndex(row.ownerIndex)\n }}\n onDragLeave={() => setDragOverIndex(null)}\n onDrop={() => {\n const drag = dragRowRef.current\n if (!drag) return\n if (drag.elementId !== element.id) {\n onReorder(drag.elementId, row.ownerIndex)\n }\n dragRowRef.current = null\n setDragOverIndex(null)\n }}\n onDragEnd={() => {\n dragRowRef.current = null\n setDragOverIndex(null)\n }}\n className={[\n 'group flex items-center gap-1.5 py-1 pr-2 text-sm transition-colors',\n isSelected ? 'bg-[color-mix(in_srgb,var(--brand-primary)_15%,transparent)] text-[var(--text-primary)]' : 'hover:bg-[hsl(var(--accent))] text-[var(--text-secondary)]',\n dragOverIndex === row.ownerIndex ? 'border-t border-[var(--brand-primary)]' : '',\n ].join(' ')}\n style={{ paddingLeft: 8 + row.depth * INDENT_PX }}\n >\n {isRenaming ? (\n <input\n autoFocus\n value={renameValue}\n onChange={(event) => setRenameValue(event.target.value)}\n onBlur={() => commitRename(element.id)}\n onKeyDown={(event) => {\n if (event.key === 'Enter') commitRename(element.id)\n if (event.key === 'Escape') setRenamingId(null)\n event.stopPropagation()\n }}\n className=\"min-w-0 flex-1 rounded border border-[var(--border-default)] bg-[var(--bg-input)] px-1 py-0 text-sm text-[var(--text-primary)] focus:border-[var(--brand-primary)]\"\n />\n ) : (\n // Selection is a real button (sibling of the eye/lock buttons) so\n // the row is not an interactive control nesting interactive\n // controls (axe: nested-interactive).\n <button\n type=\"button\"\n onClick={(event) => onSelect(element.id, event.metaKey || event.ctrlKey)}\n onDoubleClick={() => {\n if (canWrite) startRename(element)\n }}\n title={element.name}\n className=\"flex min-w-0 flex-1 cursor-pointer items-center gap-1.5 bg-transparent text-left\"\n >\n <KindIcon kind={element.kind} className=\"h-3.5 w-3.5 shrink-0 opacity-60\" />\n {element.slot ? <SlotGlyph className=\"h-3 w-3 shrink-0 text-[var(--brand-primary)]\" /> : null}\n <span className=\"min-w-0 flex-1 truncate\">{element.name}</span>\n </button>\n )}\n\n {/* Visibility toggle */}\n <button\n type=\"button\"\n aria-label={element.visible ? 'Hide element' : 'Show element'}\n aria-pressed={!element.visible}\n onClick={(event) => {\n event.stopPropagation()\n if (canWrite) onSetAttrs(element.id, { visible: !element.visible })\n }}\n disabled={!canWrite}\n className={[\n 'shrink-0 rounded p-0.5 transition-opacity group-hover:opacity-100 hover:opacity-100 focus-visible:opacity-100',\n element.visible ? 'opacity-0' : 'opacity-100',\n ].join(' ')}\n >\n {element.visible\n ? <EyeGlyph className=\"h-3.5 w-3.5\" />\n : <EyeOffGlyph className=\"h-3.5 w-3.5 text-[var(--text-muted)]\" />}\n </button>\n\n {/* Lock toggle */}\n <button\n type=\"button\"\n aria-label={element.locked ? 'Unlock element' : 'Lock element'}\n aria-pressed={!!element.locked}\n onClick={(event) => {\n event.stopPropagation()\n if (canWrite) onSetAttrs(element.id, { locked: !element.locked })\n }}\n disabled={!canWrite}\n className={[\n 'shrink-0 rounded p-0.5 transition-opacity group-hover:opacity-100 hover:opacity-100 focus-visible:opacity-100',\n element.locked ? 'opacity-100' : 'opacity-0',\n ].join(' ')}\n >\n {element.locked\n ? <LockGlyph className=\"h-3.5 w-3.5 text-[var(--text-warning)]\" />\n : <UnlockGlyph className=\"h-3.5 w-3.5\" />}\n </button>\n </div>\n )\n })}\n\n {rows.length > LAYERS_PANEL_ROW_LIMIT ? (\n <div className=\"px-3 py-2 text-[11px] text-[var(--text-muted)]\">\n +{rows.length - LAYERS_PANEL_ROW_LIMIT} more elements — select a group to scope the list\n </div>\n ) : null}\n </div>\n </div>\n )\n}\n","/**\n * Pure layer-tree helpers: flatten a page's element array into an ordered list\n * for the LayersPanel, preserving group nesting. Extracted so render order,\n * depth, and selection math can be unit-tested independently of React.\n */\n\nimport type { SceneElement, ScenePage } from '../../design-canvas/model'\n\n/** Describe a layer row with its element, depth, grouping, ownership, and parent group details */\nexport interface LayerRow {\n element: SceneElement\n depth: number\n /** True when this row is a group whose children follow it in the list. */\n isGroup: boolean\n /** Index within its owner (page.elements or group.children). */\n ownerIndex: number\n /** Length of the owner array — needed for z-order bound math. */\n ownerLength: number\n /** Id of the containing group, or null for page-root elements. */\n parentGroupId: string | null\n}\n\n/**\n * Flatten `page.elements` into display order for the layers panel.\n *\n * Z-order is bottom→top in the element array; the layers panel shows\n * top→bottom (highest z-index first). A group row precedes its children\n * (the group is \"above\" its children in the panel, reflecting that the group\n * node itself paints last in Konva when empty; children paint inside it).\n *\n * Depth increases by 1 for each group level, starting at 0 for page-root\n * elements. Groups are expanded; collapsed state is a view concern the caller\n * tracks separately.\n */\nexport function flattenLayerTree(page: ScenePage): LayerRow[] {\n const rows: LayerRow[] = []\n\n function visit(elements: SceneElement[], depth: number, parentGroupId: string | null) {\n // Reverse so highest z-index appears at the top of the panel.\n for (let i = elements.length - 1; i >= 0; i -= 1) {\n const element = elements[i]!\n rows.push({\n element,\n depth,\n isGroup: element.kind === 'group',\n ownerIndex: i,\n ownerLength: elements.length,\n parentGroupId,\n })\n if (element.kind === 'group') {\n visit(element.children, depth + 1, element.id)\n }\n }\n }\n\n visit(page.elements, 0, null)\n return rows\n}\n\n/** The maximum number of rows the layers panel renders before truncating. */\nexport const LAYERS_PANEL_ROW_LIMIT = 500\n"],"mappings":"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","names":["useRef","useState","jsx","jsxs","useState","useRef"]}
|
|
1
|
+
{"version":3,"sources":["../../src/design-canvas-react/components/CanvasInsertPanel.tsx","../../src/design-canvas-react/components/LayersPanel.tsx","../../src/design-canvas-react/components/layer-tree.ts"],"sourcesContent":["/**\n * The human \"add element to the canvas\" side panel — a reusable left rail any\n * canvas consumer can mount so a person (not just the agent) can put images,\n * shapes, and text on the canvas. Designed for the `renderSidePanel` slot of\n * {@link DesignCanvasProps}; pass `renderSidePanel={(ctx) => <CanvasInsertPanel … />}`.\n *\n * Endpoints are per-app, so every I/O path is a callback — no product route is\n * hardcoded:\n * - `onUploadImage(file) => Promise<url>` — optional. When provided, the host\n * stores the file and returns the src to insert. The url MUST be http(s) or\n * a rooted `/api/` path (enforced by `assertSceneMediaSrc` before insertion);\n * a `data:` url is rejected, matching the scene model's media boundary.\n * - `loadGenerations?()` — optional provider for \"already generated in this\n * workspace\" images; omit to hide the tab.\n * - `templates?` — optional template set; defaults to {@link DEFAULT_INSERT_TEMPLATES}.\n * A host-passed (composed) set labels its tab \"Templates\"; the built-in\n * primitives label it \"Elements\".\n *\n * Insertion goes through `onInsert`, which the host wires to its\n * `onApplyOperations` pipeline (server-validated, undoable) — the same path\n * every other edit takes.\n *\n * Tokens/icons follow the canvas convention: CSS-var design tokens and inline\n * SVG glyphs, no icon-library or Tailwind-semantic-token dependency.\n */\n\nimport { useEffect, useRef, useState, type ReactElement } from 'react'\nimport type { SceneOperation } from '../../design-canvas/operations'\nimport { assertSceneMediaSrc } from '../../design-canvas/model'\nimport {\n DEFAULT_INSERT_TEMPLATES,\n buildInsertImageOp,\n type InsertPageGeometry,\n type InsertTemplate,\n} from '../insert-builders'\nimport { ImageGlyph, ShapesGlyph } from './glyphs'\n\n/** An already-generated image the panel can offer for one-click insertion. */\nexport interface InsertGeneration {\n id: string\n /** The image url to insert. Must satisfy the scene media boundary\n * (http(s) or rooted `/api/`); rejected otherwise at insert time. */\n url: string\n /** Optional prompt/label shown as the tile's title. */\n label?: string\n}\n\nexport interface CanvasInsertPanelProps {\n canWrite: boolean\n /** The active page new elements are added to. */\n page: InsertPageGeometry\n /** Submit operations through the host's apply pipeline. */\n onInsert(operations: SceneOperation[]): Promise<unknown>\n /** Store an uploaded file and return its src (http(s) or rooted `/api/`). */\n onUploadImage?(file: File): Promise<string>\n /** Optional provider for the Generations tab; omit to hide it. */\n loadGenerations?(): Promise<InsertGeneration[]>\n /** Drop-in templates; defaults to the built-in starter set. */\n templates?: readonly InsertTemplate[]\n /** Accepted upload mime types. Default: PNG/JPEG/GIF/WebP. */\n accept?: string\n className?: string\n}\n\ntype Tab = 'uploads' | 'templates' | 'generations'\n\nconst DEFAULT_ACCEPT = 'image/png,image/jpeg,image/gif,image/webp'\n\n/** Read an image url's natural dimensions in the browser; resolves `{0,0}` when\n * Image is unavailable (SSR/tests), the load fails, or the probe times out —\n * builders fall back to the size cap, so a failed probe still inserts a sensibly\n * sized element rather than blocking insertion. */\nconst PROBE_TIMEOUT_MS = 4000\n\nfunction probeImageSize(url: string): Promise<{ width: number; height: number }> {\n return new Promise((resolve) => {\n if (typeof Image === 'undefined') return resolve({ width: 0, height: 0 })\n let settled = false\n const finish = (size: { width: number; height: number }) => {\n if (settled) return\n settled = true\n resolve(size)\n }\n const img = new Image()\n img.onload = () => finish({ width: img.naturalWidth, height: img.naturalHeight })\n img.onerror = () => finish({ width: 0, height: 0 })\n // A URL that never loads (or an environment that never fires load/error)\n // must not hang the insert forever.\n setTimeout(() => finish({ width: 0, height: 0 }), PROBE_TIMEOUT_MS)\n img.src = url\n })\n}\n\nfunction UploadGlyph({ className }: { className?: string }) {\n return (\n <svg className={className} viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\" strokeLinecap=\"round\" strokeLinejoin=\"round\" aria-hidden>\n <path d=\"M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4M7 9l5-5 5 5M12 4v12\" />\n </svg>\n )\n}\n\nfunction SparkleGlyph({ className }: { className?: string }) {\n return (\n <svg className={className} viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\" strokeLinecap=\"round\" strokeLinejoin=\"round\" aria-hidden>\n <path d=\"M12 3v3m0 12v3M3 12h3m12 0h3M5.6 5.6l2.1 2.1m8.6 8.6 2.1 2.1m0-12.8-2.1 2.1M7.7 16.3l-2.1 2.1\" />\n </svg>\n )\n}\n\nfunction SpinnerGlyph({ className }: { className?: string }) {\n return (\n <svg className={`${className ?? ''} animate-spin`} viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\" aria-hidden>\n <path d=\"M21 12a9 9 0 1 1-6.2-8.6\" strokeLinecap=\"round\" />\n </svg>\n )\n}\n\n/** Probe-page geometry for deriving a template's primary element kind. The\n * result drives only the tile preview, so the exact dimensions don't matter. */\nconst PREVIEW_PROBE_PAGE: InsertPageGeometry = { pageId: 'preview', width: 1000, height: 1000 }\n\n/** The visual a template tile shows. Derived from the first element the\n * template's (pure) `build` produces, so custom templates render correctly\n * too — not just the built-in set. */\ntype TemplateShape = 'heading' | 'body' | 'rect' | 'ellipse' | 'other'\n\n/** A text element is treated as a heading (shown as \"T\") when it reads as a\n * title — bold or set in a large face; otherwise body copy (shown as \"¶\"). */\nconst HEADING_FONT_SIZE = 32\n\nfunction templateShape(tpl: InsertTemplate): TemplateShape {\n try {\n const ops = tpl.build(PREVIEW_PROBE_PAGE)\n const added = ops.find((op) => op.type === 'add_element')\n if (!added || added.type !== 'add_element') return 'other'\n const el = added.element\n if (el.kind === 'rect') return 'rect'\n if (el.kind === 'ellipse') return 'ellipse'\n if (el.kind === 'text') {\n const isHeading = el.fontStyle.includes('bold') || el.fontSize >= HEADING_FONT_SIZE\n return isHeading ? 'heading' : 'body'\n }\n return 'other'\n } catch {\n // A throwing build can't preview; fall through to the neutral shape glyph.\n return 'other'\n }\n}\n\n/** A small visual standing in for what the template inserts, so tiles are\n * distinguishable at a glance instead of four identical text chips. */\nfunction TemplatePreview({ shape }: { shape: TemplateShape }) {\n if (shape === 'rect') {\n return <span className=\"block h-6 w-9 rounded-sm bg-[var(--brand-primary)]\" aria-hidden />\n }\n if (shape === 'ellipse') {\n return <span className=\"block h-7 w-7 rounded-full bg-[var(--brand-primary)]\" aria-hidden />\n }\n // Text tiles show a glyph: a semibold \"T\" for a heading, a paragraph mark\n // \"¶\" for body copy.\n if (shape === 'heading' || shape === 'body') {\n return (\n <span\n className={`block leading-none text-[var(--text-primary)] ${\n shape === 'heading' ? 'text-2xl font-semibold' : 'text-lg'\n }`}\n aria-hidden\n >\n {shape === 'heading' ? 'T' : '¶'}\n </span>\n )\n }\n return <ShapesGlyph className=\"h-6 w-6 text-[var(--text-muted)]\" />\n}\n\nexport function CanvasInsertPanel({\n canWrite,\n page,\n onInsert,\n onUploadImage,\n loadGenerations,\n templates,\n accept = DEFAULT_ACCEPT,\n className,\n}: CanvasInsertPanelProps) {\n // Host-passed templates are composed page sections (\"Templates\"); the\n // built-in set is single primitives, so its tab reads \"Elements\".\n const resolvedTemplates = templates ?? DEFAULT_INSERT_TEMPLATES\n const templatesLabel = templates !== undefined ? 'Templates' : 'Elements'\n const [tab, setTab] = useState<Tab>(() => (resolvedTemplates.length > 0 ? 'templates' : 'uploads'))\n const [busy, setBusy] = useState(false)\n const [error, setError] = useState('')\n const [dragOver, setDragOver] = useState(false)\n const [generations, setGenerations] = useState<InsertGeneration[]>([])\n const [generationsLoaded, setGenerationsLoaded] = useState(false)\n const fileInputRef = useRef<HTMLInputElement | null>(null)\n\n useEffect(() => {\n if (tab === 'uploads' && !onUploadImage) {\n setTab(resolvedTemplates.length > 0 ? 'templates' : loadGenerations ? 'generations' : 'uploads')\n }\n if (tab === 'templates' && resolvedTemplates.length === 0) {\n setTab(onUploadImage ? 'uploads' : loadGenerations ? 'generations' : 'uploads')\n }\n if (tab === 'generations' && !loadGenerations) {\n setTab(resolvedTemplates.length > 0 ? 'templates' : onUploadImage ? 'uploads' : 'templates')\n }\n }, [tab, resolvedTemplates.length, onUploadImage, loadGenerations])\n\n useEffect(() => {\n if (tab !== 'generations' || generationsLoaded || !loadGenerations) return\n let cancelled = false\n void (async () => {\n try {\n const rows = await loadGenerations()\n if (!cancelled) setGenerations(rows.filter((g) => g.url))\n } catch (err) {\n if (!cancelled) setError(err instanceof Error ? err.message : 'Could not load generations')\n } finally {\n if (!cancelled) setGenerationsLoaded(true)\n }\n })()\n return () => {\n cancelled = true\n }\n }, [tab, generationsLoaded, loadGenerations])\n\n async function insertImageFromUrl(url: string) {\n // Validate the media boundary BEFORE probing — a data: or sandbox-local url\n // throws here (surfaced as an inline error) without a wasted image load.\n assertSceneMediaSrc(url, 'image src')\n const natural = await probeImageSize(url)\n await onInsert([buildInsertImageOp(url, natural, page)])\n }\n\n async function handleFiles(files: FileList | File[]) {\n if (!canWrite || busy) return\n if (!onUploadImage) return\n const list = Array.from(files).filter((f) => f.type.startsWith('image/'))\n if (list.length === 0) {\n setError('Only image files can be added to the canvas')\n return\n }\n setBusy(true)\n setError('')\n try {\n for (const file of list) {\n const url = await onUploadImage(file)\n await insertImageFromUrl(url)\n }\n } catch (err) {\n setError(err instanceof Error ? err.message : 'Upload failed')\n } finally {\n setBusy(false)\n }\n }\n\n async function runInsert(build: () => SceneOperation[] | Promise<SceneOperation[]>) {\n if (!canWrite || busy) return\n setBusy(true)\n setError('')\n try {\n await onInsert(await build())\n } catch (err) {\n setError(err instanceof Error ? err.message : 'Insert failed')\n } finally {\n setBusy(false)\n }\n }\n\n const tabs: Array<{ id: Tab; label: string; icon: (p: { className?: string }) => ReactElement; show: boolean }> = [\n { id: 'uploads', label: 'Uploads', icon: ImageGlyph, show: !!onUploadImage },\n { id: 'templates', label: templatesLabel, icon: ShapesGlyph, show: resolvedTemplates.length > 0 },\n { id: 'generations', label: 'Generations', icon: SparkleGlyph, show: !!loadGenerations },\n ]\n const visibleTabs = tabs.filter((t) => t.show)\n\n return (\n <div className={`flex h-full min-h-0 flex-col bg-[var(--bg-input)] text-[var(--text-primary)] ${className ?? ''}`}>\n <div className=\"flex shrink-0 border-b border-[var(--border-default)]\">\n {visibleTabs.map(({ id, label, icon: Icon }) => (\n <button\n key={id}\n type=\"button\"\n onClick={() => setTab(id)}\n // These tabs sit flush against the top and side edges of a panel\n // that clips its overflow, so the floor's outward ring is cut off on\n // three sides and reads as a stray underline. The negative offset\n // draws the same ring inside the tab; width and colour still come\n // from the tokens.\n className={`flex flex-1 items-center justify-center gap-1.5 px-2 py-2.5 text-xs font-medium transition-colors focus-visible:[outline-offset:-2px] ${\n tab === id\n ? 'border-b-2 border-[var(--brand-primary)] text-[var(--text-primary)]'\n : 'text-[var(--text-muted)] hover:text-[var(--text-primary)]'\n }`}\n >\n <Icon className=\"h-3.5 w-3.5\" />\n {label}\n </button>\n ))}\n </div>\n\n {!canWrite ? (\n <div className=\"flex flex-1 items-center justify-center p-4 text-center text-sm text-[var(--text-muted)]\">\n You have view-only access to this design.\n </div>\n ) : (\n <div className=\"min-h-0 flex-1 overflow-y-auto p-3\">\n {tab === 'uploads' && (\n <div className=\"flex flex-col gap-3\">\n <button\n type=\"button\"\n disabled={busy}\n onClick={() => fileInputRef.current?.click()}\n onDragOver={(e) => {\n e.preventDefault()\n setDragOver(true)\n }}\n onDragLeave={() => setDragOver(false)}\n onDrop={(e) => {\n e.preventDefault()\n setDragOver(false)\n if (e.dataTransfer?.files?.length) void handleFiles(e.dataTransfer.files)\n }}\n className={`flex flex-col items-center justify-center gap-2 rounded-lg border-2 border-dashed px-4 py-8 text-center transition-colors disabled:opacity-50 ${\n dragOver\n ? 'border-[var(--brand-primary)] bg-[color-mix(in_srgb,var(--brand-primary)_5%,transparent)]'\n : 'border-[var(--border-default)] hover:border-[color-mix(in_srgb,var(--brand-primary)_40%,transparent)]'\n }`}\n >\n {busy ? (\n <SpinnerGlyph className=\"h-6 w-6 text-[var(--brand-primary)]\" />\n ) : (\n <UploadGlyph className=\"h-6 w-6 text-[var(--text-muted)]\" />\n )}\n <span className=\"text-sm font-medium text-[var(--text-primary)]\">\n {busy ? 'Uploading…' : 'Drop an image or click to upload'}\n </span>\n <span className=\"text-xs text-[var(--text-muted)]\">PNG, JPEG, GIF, or WebP</span>\n </button>\n <input\n ref={fileInputRef}\n type=\"file\"\n accept={accept}\n multiple\n className=\"hidden\"\n onChange={(e) => {\n if (e.target.files?.length) void handleFiles(e.target.files)\n e.target.value = ''\n }}\n />\n </div>\n )}\n\n {tab === 'templates' && (\n <div className=\"grid grid-cols-2 gap-2\">\n {resolvedTemplates.map((tpl) => (\n <button\n key={tpl.id}\n type=\"button\"\n disabled={busy}\n onClick={() => void runInsert(() => tpl.build(page))}\n className=\"flex h-20 flex-col items-center justify-center gap-1.5 rounded-md border border-[var(--border-default)] bg-[var(--bg-input)] text-xs font-medium text-[var(--text-primary)] transition-colors hover:border-[color-mix(in_srgb,var(--brand-primary)_40%,transparent)] disabled:opacity-50\"\n >\n <span className=\"flex h-7 items-center justify-center\">\n <TemplatePreview shape={templateShape(tpl)} />\n </span>\n <span>{tpl.label}</span>\n </button>\n ))}\n </div>\n )}\n\n {tab === 'generations' && (\n <div className=\"flex flex-col gap-2\">\n {!generationsLoaded ? (\n <div className=\"flex items-center justify-center py-8 text-[var(--text-muted)]\">\n <SpinnerGlyph className=\"h-5 w-5\" />\n </div>\n ) : generations.length === 0 ? (\n <p className=\"px-1 py-6 text-center text-sm text-[var(--text-muted)]\">\n No generated images yet. Ask the agent to generate one.\n </p>\n ) : (\n <div className=\"grid grid-cols-2 gap-2\">\n {generations.map((gen) => (\n <button\n key={gen.id}\n type=\"button\"\n disabled={busy}\n onClick={() => {\n if (busy || !canWrite) return\n setBusy(true)\n setError('')\n insertImageFromUrl(gen.url)\n .catch((err) => setError(err instanceof Error ? err.message : 'Insert failed'))\n .finally(() => setBusy(false))\n }}\n title={gen.label}\n className=\"group relative aspect-square overflow-hidden rounded-md border border-[var(--border-default)] bg-[var(--bg-input)] transition-colors hover:border-[var(--brand-primary)] disabled:opacity-50\"\n >\n {/* eslint-disable-next-line jsx-a11y/img-redundant-alt */}\n <img src={gen.url} alt={gen.label || 'Generated image'} className=\"h-full w-full object-cover\" />\n </button>\n ))}\n </div>\n )}\n </div>\n )}\n\n {error ? <p className=\"mt-3 text-xs leading-5 text-[var(--text-danger,#dc2626)]\">{error}</p> : null}\n </div>\n )}\n </div>\n )\n}\n","/**\n * Layers panel — reverse-z list of the active page's elements. Highest z-index\n * is at the top. Groups show their children indented beneath them. Clicking\n * selects an element; meta-click adds to selection. Double-clicking the name\n * opens an inline rename input. Eye and lock icons toggle visibility/locked.\n * Rows can be drag-reordered within the list (siblings only; cross-group reorder\n * is a pending integrator concern). Capped at LAYERS_PANEL_ROW_LIMIT rows.\n */\n\nimport { useCallback, useMemo, useRef, useState } from 'react'\nimport type { SceneElement, ScenePage } from '../../design-canvas/model'\nimport { flattenLayerTree, LAYERS_PANEL_ROW_LIMIT } from './layer-tree'\nimport {\n EllipseGlyph,\n EyeGlyph,\n EyeOffGlyph,\n GroupGlyph,\n ImageGlyph,\n LineGlyph,\n LockGlyph,\n RectGlyph,\n SlotGlyph,\n TextGlyph,\n UnlockGlyph,\n VideoGlyph,\n} from './glyphs'\nimport type { SceneElementKind } from '../../design-canvas/model'\n\nfunction KindIcon({ kind, className }: { kind: SceneElementKind; className?: string }) {\n switch (kind) {\n case 'rect': return <RectGlyph className={className} />\n case 'ellipse': return <EllipseGlyph className={className} />\n case 'line': return <LineGlyph className={className} />\n case 'text': return <TextGlyph className={className} />\n case 'image': return <ImageGlyph className={className} />\n case 'video': return <VideoGlyph className={className} />\n case 'group': return <GroupGlyph className={className} />\n }\n}\n\nexport interface LayersPanelProps {\n page: ScenePage\n selectedElementIds: string[]\n canWrite: boolean\n /** Emit a set_attrs command for the given element. */\n onSetAttrs(elementId: string, attrs: Partial<Pick<SceneElement, 'name' | 'visible' | 'locked'>>): void\n /** Emit a reorder_element command. */\n onReorder(elementId: string, toIndex: number): void\n onSelect(elementId: string, additive: boolean): void\n}\n\nconst INDENT_PX = 16\n\nexport function LayersPanel({ page, selectedElementIds, canWrite, onSetAttrs, onReorder, onSelect }: LayersPanelProps) {\n const rows = useMemo(() => flattenLayerTree(page), [page])\n const [renamingId, setRenamingId] = useState<string | null>(null)\n const [renameValue, setRenameValue] = useState('')\n\n // Drag-reorder state: we track the dragged row's ownerIndex and the target\n // index within the SAME owner. Cross-owner reorder is not supported here.\n const dragRowRef = useRef<{ elementId: string; ownerIndex: number; ownerLength: number } | null>(null)\n const [dragOverIndex, setDragOverIndex] = useState<number | null>(null)\n\n function startRename(element: SceneElement) {\n setRenamingId(element.id)\n setRenameValue(element.name)\n }\n\n function commitRename(elementId: string) {\n const name = renameValue.trim()\n if (name.length > 0) onSetAttrs(elementId, { name })\n setRenamingId(null)\n }\n\n const visible = rows.length > LAYERS_PANEL_ROW_LIMIT ? rows.slice(0, LAYERS_PANEL_ROW_LIMIT) : rows\n\n return (\n <div className=\"flex h-full flex-col overflow-hidden text-[var(--text-primary)]\">\n <div className=\"shrink-0 border-b border-[var(--border-default)] px-3 py-1.5 text-xs font-semibold uppercase tracking-[0.05em] text-[var(--text-muted)]\">\n Layers\n </div>\n\n <div className=\"min-h-0 flex-1 overflow-y-auto\">\n {visible.length === 0 ? (\n <div className=\"px-3 py-2 text-xs text-[var(--text-muted)]\">No layers yet</div>\n ) : null}\n {visible.map((row) => {\n const { element } = row\n const isSelected = selectedElementIds.includes(element.id)\n const isRenaming = renamingId === element.id\n\n return (\n <div\n key={element.id}\n data-layer-row={element.id}\n draggable={canWrite}\n onDragStart={() => {\n dragRowRef.current = {\n elementId: element.id,\n ownerIndex: row.ownerIndex,\n ownerLength: row.ownerLength,\n }\n }}\n onDragOver={(event) => {\n if (!dragRowRef.current) return\n // Only allow reorder within the same owner (same parentGroupId + depth)\n event.preventDefault()\n setDragOverIndex(row.ownerIndex)\n }}\n onDragLeave={() => setDragOverIndex(null)}\n onDrop={() => {\n const drag = dragRowRef.current\n if (!drag) return\n if (drag.elementId !== element.id) {\n onReorder(drag.elementId, row.ownerIndex)\n }\n dragRowRef.current = null\n setDragOverIndex(null)\n }}\n onDragEnd={() => {\n dragRowRef.current = null\n setDragOverIndex(null)\n }}\n className={[\n 'group flex items-center gap-1.5 py-1 pr-2 text-sm transition-colors',\n isSelected ? 'bg-[color-mix(in_srgb,var(--brand-primary)_15%,transparent)] text-[var(--text-primary)]' : 'hover:bg-[hsl(var(--accent))] text-[var(--text-secondary)]',\n dragOverIndex === row.ownerIndex ? 'border-t border-[var(--brand-primary)]' : '',\n ].join(' ')}\n style={{ paddingLeft: 8 + row.depth * INDENT_PX }}\n >\n {isRenaming ? (\n <input\n autoFocus\n value={renameValue}\n onChange={(event) => setRenameValue(event.target.value)}\n onBlur={() => commitRename(element.id)}\n onKeyDown={(event) => {\n if (event.key === 'Enter') commitRename(element.id)\n if (event.key === 'Escape') setRenamingId(null)\n event.stopPropagation()\n }}\n className=\"min-w-0 flex-1 rounded border border-[var(--border-default)] bg-[var(--bg-input)] px-1 py-0 text-sm text-[var(--text-primary)] focus:border-[var(--brand-primary)]\"\n />\n ) : (\n // Selection is a real button (sibling of the eye/lock buttons) so\n // the row is not an interactive control nesting interactive\n // controls (axe: nested-interactive).\n <button\n type=\"button\"\n onClick={(event) => onSelect(element.id, event.metaKey || event.ctrlKey)}\n onDoubleClick={() => {\n if (canWrite) startRename(element)\n }}\n title={element.name}\n className=\"flex min-w-0 flex-1 cursor-pointer items-center gap-1.5 bg-transparent text-left\"\n >\n <KindIcon kind={element.kind} className=\"h-3.5 w-3.5 shrink-0 opacity-60\" />\n {element.slot ? (\n <span title={`Bound to slot: ${element.slot}`} className=\"inline-flex shrink-0\">\n <SlotGlyph className=\"h-3 w-3 text-[var(--brand-primary)]\" />\n </span>\n ) : null}\n <span className=\"min-w-0 flex-1 truncate\">{element.name}</span>\n </button>\n )}\n\n {/* Visibility toggle */}\n <button\n type=\"button\"\n aria-label={element.visible ? 'Hide element' : 'Show element'}\n aria-pressed={!element.visible}\n onClick={(event) => {\n event.stopPropagation()\n if (canWrite) onSetAttrs(element.id, { visible: !element.visible })\n }}\n disabled={!canWrite}\n className={[\n 'shrink-0 rounded p-0.5 transition-opacity group-hover:opacity-100 hover:opacity-100 focus-visible:opacity-100',\n element.visible ? 'opacity-0' : 'opacity-100',\n ].join(' ')}\n >\n {element.visible\n ? <EyeGlyph className=\"h-3.5 w-3.5\" />\n : <EyeOffGlyph className=\"h-3.5 w-3.5 text-[var(--text-muted)]\" />}\n </button>\n\n {/* Lock toggle */}\n <button\n type=\"button\"\n aria-label={element.locked ? 'Unlock element' : 'Lock element'}\n aria-pressed={!!element.locked}\n onClick={(event) => {\n event.stopPropagation()\n if (canWrite) onSetAttrs(element.id, { locked: !element.locked })\n }}\n disabled={!canWrite}\n className={[\n 'shrink-0 rounded p-0.5 transition-opacity group-hover:opacity-100 hover:opacity-100 focus-visible:opacity-100',\n element.locked ? 'opacity-100' : 'opacity-0',\n ].join(' ')}\n >\n {element.locked\n ? <LockGlyph className=\"h-3.5 w-3.5 text-[var(--text-warning)]\" />\n : <UnlockGlyph className=\"h-3.5 w-3.5\" />}\n </button>\n </div>\n )\n })}\n\n {rows.length > LAYERS_PANEL_ROW_LIMIT ? (\n <div className=\"px-3 py-2 text-xs text-[var(--text-muted)]\">\n +{rows.length - LAYERS_PANEL_ROW_LIMIT} more elements — select a group to scope the list\n </div>\n ) : null}\n </div>\n </div>\n )\n}\n","/**\n * Pure layer-tree helpers: flatten a page's element array into an ordered list\n * for the LayersPanel, preserving group nesting. Extracted so render order,\n * depth, and selection math can be unit-tested independently of React.\n */\n\nimport type { SceneElement, ScenePage } from '../../design-canvas/model'\n\n/** Describe a layer row with its element, depth, grouping, ownership, and parent group details */\nexport interface LayerRow {\n element: SceneElement\n depth: number\n /** True when this row is a group whose children follow it in the list. */\n isGroup: boolean\n /** Index within its owner (page.elements or group.children). */\n ownerIndex: number\n /** Length of the owner array — needed for z-order bound math. */\n ownerLength: number\n /** Id of the containing group, or null for page-root elements. */\n parentGroupId: string | null\n}\n\n/**\n * Flatten `page.elements` into display order for the layers panel.\n *\n * Z-order is bottom→top in the element array; the layers panel shows\n * top→bottom (highest z-index first). A group row precedes its children\n * (the group is \"above\" its children in the panel, reflecting that the group\n * node itself paints last in Konva when empty; children paint inside it).\n *\n * Depth increases by 1 for each group level, starting at 0 for page-root\n * elements. Groups are expanded; collapsed state is a view concern the caller\n * tracks separately.\n */\nexport function flattenLayerTree(page: ScenePage): LayerRow[] {\n const rows: LayerRow[] = []\n\n function visit(elements: SceneElement[], depth: number, parentGroupId: string | null) {\n // Reverse so highest z-index appears at the top of the panel.\n for (let i = elements.length - 1; i >= 0; i -= 1) {\n const element = elements[i]!\n rows.push({\n element,\n depth,\n isGroup: element.kind === 'group',\n ownerIndex: i,\n ownerLength: elements.length,\n parentGroupId,\n })\n if (element.kind === 'group') {\n visit(element.children, depth + 1, element.id)\n }\n }\n }\n\n visit(page.elements, 0, null)\n return rows\n}\n\n/** The maximum number of rows the layers panel renders before truncating. */\nexport const LAYERS_PANEL_ROW_LIMIT = 500\n"],"mappings":"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","names":["useRef","useState","jsx","jsxs","useState","useRef"]}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// src/intakes-react/lazy.tsx
|
|
2
2
|
import { lazy } from "react";
|
|
3
3
|
var IntakeInterviewLazy = lazy(
|
|
4
|
-
() => import("../IntakeInterview-
|
|
4
|
+
() => import("../IntakeInterview-OKT6DCBN.js").then((m) => ({ default: m.IntakeInterview }))
|
|
5
5
|
);
|
|
6
6
|
export {
|
|
7
7
|
IntakeInterviewLazy
|
package/dist/platform/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export { c as AdminGuardOptions, d as AssertBillableBalanceOptions, A as AuthGuard, e as AuthGuardOptions, B as BetterAuthSessionCookieMinterOptions, f as BetterAuthSessionCookieSource, g as BillableBalanceState, G as GuardResolution, S as SsoStateConfig, b as TangleSsoAccountStore, a as TangleSsoAuthClient, h as TangleSsoExchangeResult, i as TangleSsoHandlerOptions, T as TangleSsoHandlers, j as TangleSsoSessionCookieArgs, k as TangleSsoUserCreateError, l as assertBillableBalance, m as createAdminGuard, n as createAuthGuard, o as createBetterAuthSessionCookieMinter, p as createSignedSsoState, q as createTangleSsoHandlers, r as guardResolution, s as parseAdminEmails, t as signSessionCookieValue, v as verifySignedSsoState } from '../sso-Cm00S61u.js';
|
|
2
2
|
import { a as TangleExecutionEnvironment, b as TangleExecutionKeySource } from '../model-CdCDfBA9.js';
|
|
3
|
-
export { D as DEFAULT_SEAT_BILLING_ENABLED_ENV_VAR, a as DEFAULT_TANGLE_TIER_POLICY, F as FREE_TIER_SPEND_CAP_USD, P as PlatformBalanceSnapshot, b as PlatformBillingHttp, c as PlatformBillingHttpError, d as PlatformBillingHttpOptions, e as PlatformIdentityStore, f as PlatformSubscriptionInfo, g as PlatformUsageProductRow, h as ProductEntitlement, i as ProductSeatOffer, j as ProductSeatOfferPeriod, S as SeatBillingFlagOptions, k as SeatStatus, T as TanglePlanTier, l as TangleTierPolicy, m as TangleTierState, n as createPlatformBillingHttp, o as createTanglePlatformBillingClient, p as getProductEntitlement, q as isPlatformBillingHttpError, r as isProductEntitled, s as isSeatBillingEnabled, t as normalizeTanglePlanTier, u as readTangleTierState, v as seatCheckoutUrl } from '../billing-
|
|
3
|
+
export { D as DEFAULT_SEAT_BILLING_ENABLED_ENV_VAR, a as DEFAULT_TANGLE_TIER_POLICY, F as FREE_TIER_SPEND_CAP_USD, P as PlatformBalanceSnapshot, b as PlatformBillingHttp, c as PlatformBillingHttpError, d as PlatformBillingHttpOptions, e as PlatformIdentityStore, f as PlatformSubscriptionInfo, g as PlatformUsageProductRow, h as ProductEntitlement, i as ProductSeatOffer, j as ProductSeatOfferPeriod, S as SeatBillingFlagOptions, k as SeatStatus, T as TanglePlanTier, l as TangleTierPolicy, m as TangleTierState, n as createPlatformBillingHttp, o as createTanglePlatformBillingClient, p as getProductEntitlement, q as isPlatformBillingHttpError, r as isProductEntitled, s as isSeatBillingEnabled, t as normalizeTanglePlanTier, u as readTangleTierState, v as seatCheckoutUrl } from '../billing-BuNlaKlE.js';
|
|
4
4
|
import '../billing/index.js';
|
|
5
5
|
|
|
6
6
|
/**
|
package/dist/platform/index.js
CHANGED
|
@@ -13,7 +13,8 @@ import {
|
|
|
13
13
|
} from "../chunk-YABHE2QY.js";
|
|
14
14
|
import "../chunk-F7VH4G5C.js";
|
|
15
15
|
import {
|
|
16
|
-
resolveTangleDevOrUserKey
|
|
16
|
+
resolveTangleDevOrUserKey,
|
|
17
|
+
resolveTangleExecutionEnvironment
|
|
17
18
|
} from "../chunk-JML7WKWU.js";
|
|
18
19
|
|
|
19
20
|
// src/platform/hub.ts
|
|
@@ -210,8 +211,8 @@ function createPlatformBillingHttp(opts) {
|
|
|
210
211
|
currentPeriodEnd: data.currentPeriodEnd ?? null,
|
|
211
212
|
lifetimeSpentUsd: data.lifetimeSpentUsd ?? 0,
|
|
212
213
|
hasSeat,
|
|
213
|
-
//
|
|
214
|
-
onFreeTier:
|
|
214
|
+
// Product-funded free access is retired. Ignore stale server signals.
|
|
215
|
+
onFreeTier: false,
|
|
215
216
|
...offer ? { offer } : {}
|
|
216
217
|
};
|
|
217
218
|
},
|
|
@@ -271,33 +272,32 @@ async function readTangleTierState(http, userApiKey, policy = DEFAULT_TANGLE_TIE
|
|
|
271
272
|
...policy[subscription.tier]
|
|
272
273
|
};
|
|
273
274
|
}
|
|
274
|
-
var FREE_TIER_SPEND_CAP_USD =
|
|
275
|
+
var FREE_TIER_SPEND_CAP_USD = 0;
|
|
275
276
|
var DEFAULT_SEAT_BILLING_ENABLED_ENV_VAR = "SEAT_BILLING_ENABLED";
|
|
276
277
|
function isSeatBillingEnabled(opts = {}) {
|
|
277
278
|
const env = opts.env ?? (typeof process !== "undefined" ? process.env : void 0);
|
|
278
|
-
if (!env) return
|
|
279
|
+
if (!env) return true;
|
|
279
280
|
const flag = env[opts.flagEnvVar ?? DEFAULT_SEAT_BILLING_ENABLED_ENV_VAR]?.trim().toLowerCase();
|
|
280
|
-
|
|
281
|
+
if (flag) return !["false", "0", "off", "disabled", "no"].includes(flag);
|
|
282
|
+
const environment = resolveTangleExecutionEnvironment(env);
|
|
283
|
+
return environment !== "development" && environment !== "test";
|
|
281
284
|
}
|
|
282
285
|
async function getProductEntitlement(http, userApiKey, productId, flag = true) {
|
|
283
|
-
if (!flag
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
} catch {
|
|
287
|
-
return failOpenEntitlement();
|
|
288
|
-
}
|
|
286
|
+
if (!flag) return unavailableEntitlement();
|
|
287
|
+
if (!userApiKey) return unavailableEntitlement();
|
|
288
|
+
return http.getProductEntitlement(userApiKey, productId);
|
|
289
289
|
}
|
|
290
|
-
function
|
|
290
|
+
function unavailableEntitlement() {
|
|
291
291
|
return {
|
|
292
|
-
seatStatus: "
|
|
292
|
+
seatStatus: "none",
|
|
293
293
|
currentPeriodEnd: null,
|
|
294
294
|
lifetimeSpentUsd: 0,
|
|
295
|
-
hasSeat:
|
|
295
|
+
hasSeat: false,
|
|
296
296
|
onFreeTier: false
|
|
297
297
|
};
|
|
298
298
|
}
|
|
299
299
|
function isProductEntitled(ent) {
|
|
300
|
-
return ent.hasSeat
|
|
300
|
+
return ent.hasSeat;
|
|
301
301
|
}
|
|
302
302
|
function createTanglePlatformBillingClient(http, identity) {
|
|
303
303
|
return {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/platform/hub.ts","../../src/platform/billing.ts"],"sourcesContent":["/**\n * Integrations-hub proxy routes: the app-side surface that forwards an\n * authenticated user's requests to the platform's `/v1/integrations/*` API\n * using their stored platform key. Auth, key lookup, and the wire client are\n * structural seams (`HubProxyContext`); error detection is by name + shape so\n * it survives bundlers duplicating module instances.\n */\n\nimport {\n resolveTangleDevOrUserKey,\n type TangleExecutionEnvironment,\n type TangleExecutionKeySource,\n} from '../runtime/model'\n\n/** Hub bearer provenance mirrors the execution-key source union. */\nexport type TangleHubBearerSource = TangleExecutionKeySource\n\n/** Represent a resolved bearer token with its associated TangleHub bearer source */\nexport interface ResolvedTangleHubBearer {\n bearer: string\n source: TangleHubBearerSource\n}\n\n/** Resolve options required to obtain a user's TangleHub bearer token including environment and API key retrieval */\nexport interface ResolveUserTangleHubBearerOptions {\n userId: string\n /** Deployment context. Only local development may use env credentials. */\n environment?: TangleExecutionEnvironment\n /** Env to read for the local-development bearer. */\n env?: Record<string, string | undefined>\n /** App-owned lookup for the caller's linked platform API key. */\n getUserApiKey: () => string | null | undefined | Promise<string | null | undefined>\n}\n\n/** Resolve options for retrieving a TangleHub bearer token for a specified user */\nexport interface ResolveUserTangleHubBearerForUserOptions<UserId = string> {\n userId: UserId\n environment?: TangleExecutionEnvironment\n env?: Record<string, string | undefined>\n getUserApiKey: (userId: UserId) => string | null | undefined | Promise<string | null | undefined>\n}\n\n/** Represent missing Tangle platform link error for a specified user ID */\nexport class TangleBearerMissingError extends Error {\n constructor(readonly userId: string) {\n super(`No Tangle platform link for user ${userId}`)\n this.name = 'TangleBearerMissingError'\n }\n}\n\n/**\n * Resolve the Tangle bearer used by the integration hub proxy.\n *\n * Local development may use a server env key so apps can exercise the hub\n * without completing cross-site SSO. Deployed contexts must use the caller's\n * linked platform key; this keeps integration ownership aligned with the user.\n */\nexport async function resolveUserTangleHubBearer(\n opts: ResolveUserTangleHubBearerOptions,\n): Promise<ResolvedTangleHubBearer> {\n const resolved = await resolveTangleDevOrUserKey({\n environment: opts.environment,\n env: opts.env,\n getUserApiKey: opts.getUserApiKey,\n })\n if (resolved) return { bearer: resolved.apiKey, source: resolved.source }\n\n throw new TangleBearerMissingError(opts.userId)\n}\n\n/** Resolve the TangleHub bearer token for a specified user based on provided options */\nexport async function resolveUserTangleHubBearerForUser<UserId = string>(\n opts: ResolveUserTangleHubBearerForUserOptions<UserId>,\n): Promise<ResolvedTangleHubBearer> {\n return resolveUserTangleHubBearer({\n userId: String(opts.userId),\n environment: opts.environment,\n env: opts.env,\n getUserApiKey: () => opts.getUserApiKey(opts.userId),\n })\n}\n\n/** Structural guard (name + userId shape) — robust when the error class is\n * constructed in a different module instance than the one checking it. */\nexport function isTangleBearerMissingError(error: unknown): error is TangleBearerMissingError {\n return (\n error instanceof Error &&\n error.name === 'TangleBearerMissingError' &&\n typeof (error as { userId?: unknown }).userId === 'string'\n )\n}\n\n/** Structural detection of the platform hub wire error (name + numeric status). */\nexport function isPlatformHubErrorLike(error: unknown): error is Error & { status: number; code?: string } {\n return (\n error instanceof Error &&\n error.name === 'PlatformHubError' &&\n typeof (error as { status?: unknown }).status === 'number'\n )\n}\n\n/** Structural subset of the platform hub wire client — extra methods are fine. */\nexport interface HubClientLike {\n catalog(): Promise<unknown>\n listConnections(): Promise<unknown>\n revokeConnection(connectionId: string): Promise<unknown>\n startAuth(input: {\n providerId: string\n connectorId: string\n returnUrl: string\n requestedScopes?: string[]\n }): Promise<{ authorizationUrl: string; state: string }>\n listHealthchecks(): Promise<unknown>\n}\n\n/** Define methods to require user ID, get bearer token, and create a hub client bound to the bearer */\nexport interface HubProxyContext {\n /** Resolve the authenticated user id. Throw the app's own auth Response /\n * redirect to reject — it propagates untouched. */\n requireUserId(request: Request): Promise<string>\n /** The user's platform bearer; throw `TangleBearerMissingError` when unlinked. */\n getBearer(userId: string): Promise<string>\n /** A hub client bound to the bearer. */\n createHubClient(bearer: string): HubClientLike\n}\n\n/** Define arguments for configuring a proxy route with request and optional parameters */\nexport interface HubProxyRouteArgs {\n request: Request\n params?: Record<string, string | undefined>\n}\n\n/** Define routes for hub proxy handling catalog, connections, healthchecks, and authorization actions */\nexport interface HubProxyRoutes {\n /** GET → `{ catalog }`. */\n catalog(args: HubProxyRouteArgs): Promise<Response>\n /** GET → `{ connections }`. */\n connections(args: HubProxyRouteArgs): Promise<Response>\n /** DELETE → the platform revocation result verbatim; 405 otherwise. */\n connectionDelete(args: { request: Request; params: { connectionId: string } }): Promise<Response>\n /** GET → `{ healthchecks }`. */\n healthchecks(args: HubProxyRouteArgs): Promise<Response>\n /** POST `{ providerId, connectorId, returnUrl, requestedScopes? }` →\n * `{ authorizationUrl, state }`; 405 non-POST; 400 on bad JSON / missing fields. */\n authStart(args: HubProxyRouteArgs): Promise<Response>\n}\n\ninterface StartAuthBody {\n providerId?: string\n connectorId?: string\n returnUrl?: string\n requestedScopes?: string[]\n}\n\n/** Resolve hub proxy routes with authentication and error handling based on the given context */\nexport function createHubProxyRoutes(ctx: HubProxyContext): HubProxyRoutes {\n /** Auth runs OUTSIDE the proxy try/catch so the app's auth throw (redirect\n * Response etc.) is never swallowed; bearer + platform errors are mapped. */\n async function proxy(request: Request, call: (hub: HubClientLike) => Promise<Response>): Promise<Response> {\n const userId = await ctx.requireUserId(request)\n try {\n const bearer = await ctx.getBearer(userId)\n return await call(ctx.createHubClient(bearer))\n } catch (err) {\n if (isTangleBearerMissingError(err)) {\n return Response.json({ error: 'tangle_link_required' }, { status: 412 })\n }\n if (isPlatformHubErrorLike(err)) {\n return Response.json({ error: err.message, code: err.code }, { status: err.status })\n }\n throw err\n }\n }\n\n return {\n catalog: ({ request }) => proxy(request, async (hub) => Response.json({ catalog: await hub.catalog() })),\n\n connections: ({ request }) =>\n proxy(request, async (hub) => Response.json({ connections: await hub.listConnections() })),\n\n connectionDelete: async ({ request, params }) => {\n if (request.method !== 'DELETE') {\n return Response.json({ error: 'Method not allowed' }, { status: 405 })\n }\n return proxy(request, async (hub) => Response.json(await hub.revokeConnection(params.connectionId)))\n },\n\n healthchecks: ({ request }) =>\n proxy(request, async (hub) => Response.json({ healthchecks: await hub.listHealthchecks() })),\n\n authStart: async ({ request }) => {\n if (request.method !== 'POST') {\n return Response.json({ error: 'Method not allowed' }, { status: 405 })\n }\n const userId = await ctx.requireUserId(request)\n let body: StartAuthBody\n try {\n body = (await request.json()) as StartAuthBody\n } catch {\n return Response.json({ error: 'Invalid JSON body' }, { status: 400 })\n }\n if (!body.providerId || !body.connectorId || !body.returnUrl) {\n return Response.json({ error: 'providerId, connectorId, and returnUrl are required' }, { status: 400 })\n }\n try {\n const bearer = await ctx.getBearer(userId)\n const result = await ctx.createHubClient(bearer).startAuth({\n providerId: body.providerId,\n connectorId: body.connectorId,\n returnUrl: body.returnUrl,\n requestedScopes: body.requestedScopes,\n })\n return Response.json({ authorizationUrl: result.authorizationUrl, state: result.state })\n } catch (err) {\n if (isTangleBearerMissingError(err)) {\n return Response.json({ error: 'tangle_link_required' }, { status: 412 })\n }\n if (isPlatformHubErrorLike(err)) {\n return Response.json({ error: err.message, code: err.code }, { status: err.status })\n }\n throw err\n }\n },\n }\n}\n","/**\n * Platform billing HTTP transport + tier state for apps on the shared\n * Tangle balance model (id.tangle.tools). Reads authenticate as the user via\n * their per-user platform key (the platform resolves the caller from the\n * key; service or impersonation headers on read routes are rejected). The\n * deduct write authenticates as the product service (`Bearer <serviceToken>`\n * + `X-Service-Name`) and names the target user in the body. Also provides a\n * fetch-backed implementation of the `/billing` module's\n * `PlatformBillingClient` seam (type-only import — no runtime coupling).\n */\n\nimport type { PlatformBillingClient, PlatformIdentity } from '../billing/index'\n\n/** Define available subscription tiers for the TanglePlan service */\nexport type TanglePlanTier = 'free' | 'pro' | 'enterprise'\n\n/** 'pro' | 'enterprise' pass through; anything else (null, unknown) → 'free'. */\nexport function normalizeTanglePlanTier(plan: string | null | undefined): TanglePlanTier {\n return plan === 'pro' || plan === 'enterprise' ? plan : 'free'\n}\n\n/** Represent platform billing HTTP errors with status code and detailed message */\nexport class PlatformBillingHttpError extends Error {\n constructor(\n readonly status: number,\n detail: string,\n ) {\n super(`Platform request failed (${status}): ${detail}`)\n this.name = 'PlatformBillingHttpError'\n }\n}\n\n/** Structural guard (name + numeric status) — robust across module instances. */\nexport function isPlatformBillingHttpError(error: unknown): error is PlatformBillingHttpError {\n return (\n error instanceof Error &&\n error.name === 'PlatformBillingHttpError' &&\n typeof (error as { status?: unknown }).status === 'number'\n )\n}\n\n/** Define HTTP options for platform billing including base URL, service token, product slug, fetch implementation, and timeout */\nexport interface PlatformBillingHttpOptions {\n /** Platform root, e.g. https://id.tangle.tools (trailing slashes stripped). */\n baseUrl: string\n /** Used only by `deduct()`; resolved lazily so reads never require it.\n * Throws at call time when empty. */\n serviceToken: string | (() => string)\n /** Product slug — the `X-Service-Name` header and the deduct `product` field. */\n productSlug: string\n fetchImpl?: typeof fetch\n /** Default 10 000. */\n timeoutMs?: number\n}\n\n/** Describe subscription tier and status information for a platform user */\nexport interface PlatformSubscriptionInfo {\n tier: TanglePlanTier\n status: string | null\n}\n\n/** Describe the platform balance and lifetime spending with an optional update timestamp */\nexport interface PlatformBalanceSnapshot {\n balance: number\n lifetimeSpent: number\n updatedAt?: string\n}\n\n/** Describe a product's usage and spending metrics on the platform */\nexport interface PlatformUsageProductRow {\n product: string | null\n totalSpent: number\n count: number\n}\n\n/** Lifecycle of a per-product seat subscription, mirroring the Stripe states\n * the platform persists. 'none' = the user has never held this seat. */\nexport type SeatStatus = 'none' | 'active' | 'trialing' | 'past_due' | 'canceled'\n\n/** Price and included shared-wallet credit for one seat billing period. */\nexport interface ProductSeatOfferPeriod {\n priceCents: number\n includedCreditsCents: number\n}\n\n/** Commercial terms returned by the platform's product catalog. Products use\n * this exact object for display instead of duplicating prices in UI copy. */\nexport interface ProductSeatOffer {\n currency: 'usd'\n interval: 'month'\n recurring: ProductSeatOfferPeriod\n introductory: ProductSeatOfferPeriod | null\n}\n\n/**\n * Per-product entitlement snapshot from the platform — the single read that\n * tells a product whether to show its workspace or the seat paywall. Shape\n * matches `GET /v1/billing/product-entitlement?product=<id>`.\n *\n * `hasSeat` and `onFreeTier` are computed platform-side from the raw seat row\n * + cumulative spend so the access rule is identical across products:\n * - `hasSeat` — an active/trialing seat whose period has not lapsed.\n * - `onFreeTier` — no active seat AND cumulative spend below the free cap\n * ($2 / 200¢ lifetime). Keys off lifetime spend, not wallet\n * balance, so a router top-up never re-opens free access.\n */\nexport interface ProductEntitlement {\n seatStatus: SeatStatus\n /** ISO timestamp the active seat's paid period runs until; null when none. */\n currentPeriodEnd: string | null\n /** Cumulative inference spend across the whole suite, in dollars. */\n lifetimeSpentUsd: number\n hasSeat: boolean\n onFreeTier: boolean\n /** Present when the platform exposes catalog-backed commercial terms. */\n offer?: ProductSeatOffer\n}\n\nfunction productSeatOffer(value: unknown): ProductSeatOffer | undefined {\n if (!value || typeof value !== 'object') return undefined\n const candidate = value as Record<string, unknown>\n if (candidate.currency !== 'usd' || candidate.interval !== 'month') return undefined\n\n const period = (input: unknown, allowZeroPrice: boolean): ProductSeatOfferPeriod | undefined => {\n if (!input || typeof input !== 'object') return undefined\n const data = input as Record<string, unknown>\n const priceCents = data.priceCents\n const includedCreditsCents = data.includedCreditsCents\n if (\n typeof priceCents !== 'number' ||\n !Number.isSafeInteger(priceCents) ||\n (allowZeroPrice ? priceCents < 0 : priceCents <= 0) ||\n typeof includedCreditsCents !== 'number' ||\n !Number.isSafeInteger(includedCreditsCents) ||\n includedCreditsCents < 0\n ) {\n return undefined\n }\n return { priceCents, includedCreditsCents }\n }\n\n const recurring = period(candidate.recurring, false)\n const introductory =\n candidate.introductory === null\n ? null\n : period(candidate.introductory, true)\n if (!recurring || introductory === undefined) return undefined\n return {\n currency: 'usd',\n interval: 'month',\n recurring,\n introductory,\n }\n}\n\n/** Define methods to interact with platform billing endpoints using user or service authentication */\nexport interface PlatformBillingHttp {\n /** GET /v1/plans/current (user bearer). */\n getSubscription(userApiKey: string): Promise<PlatformSubscriptionInfo>\n /** GET /v1/billing/balance (user bearer). */\n getBalance(userApiKey: string): Promise<PlatformBalanceSnapshot>\n /** GET /v1/billing/usage (user bearer). */\n getUsageByProduct(userApiKey: string): Promise<PlatformUsageProductRow[]>\n /** GET /v1/billing/product-entitlement?product=<id> (user bearer). */\n getProductEntitlement(userApiKey: string, productId: string): Promise<ProductEntitlement>\n /** POST /v1/billing/deduct (service token). */\n deduct(input: {\n platformUserId: string\n amountUsd: number\n type: string\n description: string\n referenceId: string\n }): Promise<void>\n /** Absolute URL of the platform's billing-management surface. */\n billingUrl(): string\n /** Absolute URL of the catalog-backed seat checkout for `productId`. */\n seatCheckoutUrl(productId: string): string\n}\n\n/** Create a PlatformBillingHttp instance configured with given options and default behaviors */\nexport function createPlatformBillingHttp(opts: PlatformBillingHttpOptions): PlatformBillingHttp {\n const baseUrl = opts.baseUrl.replace(/\\/+$/, '')\n if (!baseUrl) throw new Error('PlatformBillingHttpOptions.baseUrl is required')\n if (!opts.productSlug) throw new Error('PlatformBillingHttpOptions.productSlug is required')\n const fetchImpl = opts.fetchImpl ?? fetch\n const timeoutMs = opts.timeoutMs ?? 10_000\n\n function resolveServiceToken(): string {\n const token = typeof opts.serviceToken === 'function' ? opts.serviceToken() : opts.serviceToken\n if (!token) throw new Error('A platform service token is required for deduct')\n return token\n }\n\n async function request<T>(path: string, init: RequestInit, headers: Headers): Promise<T> {\n const res = await fetchImpl(`${baseUrl}${path}`, {\n ...init,\n headers,\n signal: AbortSignal.timeout(timeoutMs),\n })\n if (!res.ok) {\n const body = (await res.json().catch(() => null)) as { error?: { message?: string } } | null\n throw new PlatformBillingHttpError(res.status, body?.error?.message ?? res.statusText)\n }\n return res.json() as Promise<T>\n }\n\n function userRead<T>(userApiKey: string, path: string): Promise<T> {\n const headers = new Headers()\n headers.set('Authorization', `Bearer ${userApiKey}`)\n return request<T>(path, {}, headers)\n }\n\n return {\n async getSubscription(userApiKey) {\n const body = await userRead<{\n success: boolean\n data?: { subscription?: { plan?: string | null; status?: string | null } | null }\n }>(userApiKey, '/v1/plans/current')\n const sub = body.data?.subscription ?? null\n return { tier: normalizeTanglePlanTier(sub?.plan), status: sub?.status ?? null }\n },\n\n async getBalance(userApiKey) {\n const body = await userRead<{\n success: boolean\n data?: { balance?: number; lifetimeSpent?: number; updatedAt?: string }\n }>(userApiKey, '/v1/billing/balance')\n return {\n balance: body.data?.balance ?? 0,\n lifetimeSpent: body.data?.lifetimeSpent ?? 0,\n updatedAt: body.data?.updatedAt,\n }\n },\n\n async getUsageByProduct(userApiKey) {\n const body = await userRead<{\n success: boolean\n data?: Array<{ product?: string | null; totalSpent?: number; count?: number }>\n }>(userApiKey, '/v1/billing/usage')\n return (body.data ?? []).map((row) => ({\n product: row.product ?? null,\n totalSpent: row.totalSpent ?? 0,\n count: row.count ?? 0,\n }))\n },\n\n async getProductEntitlement(userApiKey, productId) {\n const slug = encodeURIComponent(productId)\n const body = await userRead<{\n success: boolean\n data?: {\n seatStatus?: SeatStatus | null\n currentPeriodEnd?: string | null\n lifetimeSpentUsd?: number | null\n hasSeat?: boolean | null\n onFreeTier?: boolean | null\n offer?: unknown\n }\n }>(userApiKey, `/v1/billing/product-entitlement?product=${slug}`)\n const data = body.data ?? {}\n const hasSeat = data.hasSeat === true\n const offer = productSeatOffer(data.offer)\n return {\n seatStatus: data.seatStatus ?? 'none',\n currentPeriodEnd: data.currentPeriodEnd ?? null,\n lifetimeSpentUsd: data.lifetimeSpentUsd ?? 0,\n hasSeat,\n // Free access only when there is no seat AND the platform says so.\n onFreeTier: !hasSeat && data.onFreeTier === true,\n ...(offer ? { offer } : {}),\n }\n },\n\n async deduct(input) {\n const headers = new Headers()\n headers.set('Authorization', `Bearer ${resolveServiceToken()}`)\n headers.set('X-Service-Name', opts.productSlug)\n headers.set('Content-Type', 'application/json')\n await request('/v1/billing/deduct', {\n method: 'POST',\n body: JSON.stringify({\n userId: input.platformUserId,\n amount: input.amountUsd,\n type: input.type,\n product: opts.productSlug,\n description: input.description,\n referenceId: input.referenceId,\n }),\n }, headers)\n },\n\n billingUrl() {\n return `${baseUrl}/app/billing`\n },\n\n seatCheckoutUrl(productId) {\n return seatCheckoutUrl(baseUrl, productId)\n },\n }\n}\n\n/**\n * Platform Stripe checkout URL for a product's catalog-backed seat. The\n * platform resolves the product-specific price and introductory offer from the\n * `product` query param. Mirrors the `billingUrl()` shape — a deterministic\n * platform-rooted URL with no client-side pricing decisions.\n */\nexport function seatCheckoutUrl(baseUrl: string, productId: string): string {\n const root = baseUrl.replace(/\\/+$/, '')\n return `${root}/app/billing/seat/checkout?product=${encodeURIComponent(productId)}`\n}\n\n// ── Tier policy + composed state ────────────────────────────────────────────\n\n/** Define policy settings for concurrency and overage allowance in a tangle tier */\nexport interface TangleTierPolicy {\n concurrency: number\n overageAllowed: boolean\n}\n\n/** Define default concurrency and overage policies for each TanglePlanTier level */\nexport const DEFAULT_TANGLE_TIER_POLICY: Record<TanglePlanTier, TangleTierPolicy> = {\n free: { concurrency: 1, overageAllowed: false },\n pro: { concurrency: Number.POSITIVE_INFINITY, overageAllowed: true },\n enterprise: { concurrency: Number.POSITIVE_INFINITY, overageAllowed: true },\n}\n\n/** Describe the state of a Tangle plan tier including subscription, balance, spending, and concurrency details */\nexport interface TangleTierState {\n tier: TanglePlanTier\n subscriptionStatus: string | null\n remainingBalanceUsd: number\n lifetimeSpentUsd: number\n concurrency: number\n overageAllowed: boolean\n}\n\n/**\n * Read subscription + balance and project them onto the tier policy. A\n * null/absent key fails CLOSED (free tier, zero balance) — a billable run is\n * never started against an unknown balance. Platform errors throw; callers\n * on the billable path choose their posture explicitly.\n */\nexport async function readTangleTierState(\n http: PlatformBillingHttp,\n userApiKey: string | null | undefined,\n policy: Record<TanglePlanTier, TangleTierPolicy> = DEFAULT_TANGLE_TIER_POLICY,\n): Promise<TangleTierState> {\n if (!userApiKey) {\n return {\n tier: 'free',\n subscriptionStatus: null,\n remainingBalanceUsd: 0,\n lifetimeSpentUsd: 0,\n ...policy.free,\n }\n }\n const [subscription, balance] = await Promise.all([\n http.getSubscription(userApiKey),\n http.getBalance(userApiKey),\n ])\n return {\n tier: subscription.tier,\n subscriptionStatus: subscription.status,\n remainingBalanceUsd: balance.balance,\n lifetimeSpentUsd: balance.lifetimeSpent,\n ...policy[subscription.tier],\n }\n}\n\n// ── Per-product seat entitlement ────────────────────────────────────────────\n\n/** Lifetime free-tier cap: $2 (200¢) cumulative inference spend, expressed in\n * dollars. Free product access ends once cumulative spend crosses this. */\nexport const FREE_TIER_SPEND_CAP_USD = 2\n\n/**\n * Default name of the per-app feature flag gating seat billing. While OFF the\n * entitlement read is skipped and access fails OPEN (entitled) so nothing\n * changes live until a product flips the flag.\n */\nexport const DEFAULT_SEAT_BILLING_ENABLED_ENV_VAR = 'SEAT_BILLING_ENABLED'\n\n/** Define options to configure seat billing flag environment variables and override flag name */\nexport interface SeatBillingFlagOptions {\n env?: Record<string, string | undefined>\n /** Override the flag name; default {@link DEFAULT_SEAT_BILLING_ENABLED_ENV_VAR}. */\n flagEnvVar?: string\n}\n\n/**\n * Seat billing is OFF unless the flag is explicitly truthy ('true'/'1'/'on'/\n * 'enabled'). Default OFF — pre-rollout, the paywall never engages. Returns\n * false when no env is available (browser bundles) so the client stays\n * fail-open there too.\n */\nexport function isSeatBillingEnabled(opts: SeatBillingFlagOptions = {}): boolean {\n const env =\n opts.env ??\n (typeof process !== 'undefined' ? (process.env as Record<string, string | undefined>) : undefined)\n if (!env) return false\n const flag = env[opts.flagEnvVar ?? DEFAULT_SEAT_BILLING_ENABLED_ENV_VAR]?.trim().toLowerCase()\n return flag === 'true' || flag === '1' || flag === 'on' || flag === 'enabled'\n}\n\n/**\n * Read a user's entitlement for one product. Fails OPEN: an absent key,\n * disabled flag, or unreachable seat endpoint all return a permissive snapshot\n * (`hasSeat: true`) so consumers never break pre-rollout. The platform owns the\n * `hasSeat`/`onFreeTier` computation; this client only transports + degrades\n * safely.\n *\n * @param flag — pass {@link isSeatBillingEnabled} (or your own boolean) so the\n * product owns when the gate engages. When false, no network call is made.\n */\nexport async function getProductEntitlement(\n http: Pick<PlatformBillingHttp, 'getProductEntitlement'>,\n userApiKey: string | null | undefined,\n productId: string,\n flag = true,\n): Promise<ProductEntitlement> {\n if (!flag || !userApiKey) return failOpenEntitlement()\n try {\n return await http.getProductEntitlement(userApiKey, productId)\n } catch {\n // Seat endpoint unavailable (pre-rollout platform, transient 5xx): never\n // wall a paying or grandfathered user on a transport hiccup.\n return failOpenEntitlement()\n }\n}\n\nfunction failOpenEntitlement(): ProductEntitlement {\n return {\n seatStatus: 'active',\n currentPeriodEnd: null,\n lifetimeSpentUsd: 0,\n hasSeat: true,\n onFreeTier: false,\n }\n}\n\n/** Entitled = holds an active seat OR is still inside the free tier. The one\n * predicate every product uses. */\nexport function isProductEntitled(ent: ProductEntitlement): boolean {\n return ent.hasSeat || ent.onFreeTier\n}\n\n// ── Bridge onto the /billing seam ───────────────────────────────────────────\n\n/** Define a contract for resolving platform identities based on user identifiers */\nexport interface PlatformIdentityStore {\n resolveIdentity(userId: string): Promise<PlatformIdentity | null>\n}\n\n/** Concrete fetch-backed `PlatformBillingClient<TanglePlanTier>` for\n * `createPlatformBalanceManager` (from `/billing`). */\nexport function createTanglePlatformBillingClient(\n http: PlatformBillingHttp,\n identity: PlatformIdentityStore,\n): PlatformBillingClient<TanglePlanTier> {\n return {\n resolveIdentity: (userId) => identity.resolveIdentity(userId),\n getPlan: async (apiKey) => (await http.getSubscription(apiKey)).tier,\n getBalance: async (apiKey) => {\n const snapshot = await http.getBalance(apiKey)\n return { balance: snapshot.balance, lifetimeSpent: snapshot.lifetimeSpent }\n },\n getUsageByProduct: (apiKey) => http.getUsageByProduct(apiKey),\n deduct: (input) => http.deduct(input),\n }\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/platform/hub.ts","../../src/platform/billing.ts"],"sourcesContent":["/**\n * Integrations-hub proxy routes: the app-side surface that forwards an\n * authenticated user's requests to the platform's `/v1/integrations/*` API\n * using their stored platform key. Auth, key lookup, and the wire client are\n * structural seams (`HubProxyContext`); error detection is by name + shape so\n * it survives bundlers duplicating module instances.\n */\n\nimport {\n resolveTangleDevOrUserKey,\n type TangleExecutionEnvironment,\n type TangleExecutionKeySource,\n} from '../runtime/model'\n\n/** Hub bearer provenance mirrors the execution-key source union. */\nexport type TangleHubBearerSource = TangleExecutionKeySource\n\n/** Represent a resolved bearer token with its associated TangleHub bearer source */\nexport interface ResolvedTangleHubBearer {\n bearer: string\n source: TangleHubBearerSource\n}\n\n/** Resolve options required to obtain a user's TangleHub bearer token including environment and API key retrieval */\nexport interface ResolveUserTangleHubBearerOptions {\n userId: string\n /** Deployment context. Only local development may use env credentials. */\n environment?: TangleExecutionEnvironment\n /** Env to read for the local-development bearer. */\n env?: Record<string, string | undefined>\n /** App-owned lookup for the caller's linked platform API key. */\n getUserApiKey: () => string | null | undefined | Promise<string | null | undefined>\n}\n\n/** Resolve options for retrieving a TangleHub bearer token for a specified user */\nexport interface ResolveUserTangleHubBearerForUserOptions<UserId = string> {\n userId: UserId\n environment?: TangleExecutionEnvironment\n env?: Record<string, string | undefined>\n getUserApiKey: (userId: UserId) => string | null | undefined | Promise<string | null | undefined>\n}\n\n/** Represent missing Tangle platform link error for a specified user ID */\nexport class TangleBearerMissingError extends Error {\n constructor(readonly userId: string) {\n super(`No Tangle platform link for user ${userId}`)\n this.name = 'TangleBearerMissingError'\n }\n}\n\n/**\n * Resolve the Tangle bearer used by the integration hub proxy.\n *\n * Local development may use a server env key so apps can exercise the hub\n * without completing cross-site SSO. Deployed contexts must use the caller's\n * linked platform key; this keeps integration ownership aligned with the user.\n */\nexport async function resolveUserTangleHubBearer(\n opts: ResolveUserTangleHubBearerOptions,\n): Promise<ResolvedTangleHubBearer> {\n const resolved = await resolveTangleDevOrUserKey({\n environment: opts.environment,\n env: opts.env,\n getUserApiKey: opts.getUserApiKey,\n })\n if (resolved) return { bearer: resolved.apiKey, source: resolved.source }\n\n throw new TangleBearerMissingError(opts.userId)\n}\n\n/** Resolve the TangleHub bearer token for a specified user based on provided options */\nexport async function resolveUserTangleHubBearerForUser<UserId = string>(\n opts: ResolveUserTangleHubBearerForUserOptions<UserId>,\n): Promise<ResolvedTangleHubBearer> {\n return resolveUserTangleHubBearer({\n userId: String(opts.userId),\n environment: opts.environment,\n env: opts.env,\n getUserApiKey: () => opts.getUserApiKey(opts.userId),\n })\n}\n\n/** Structural guard (name + userId shape) — robust when the error class is\n * constructed in a different module instance than the one checking it. */\nexport function isTangleBearerMissingError(error: unknown): error is TangleBearerMissingError {\n return (\n error instanceof Error &&\n error.name === 'TangleBearerMissingError' &&\n typeof (error as { userId?: unknown }).userId === 'string'\n )\n}\n\n/** Structural detection of the platform hub wire error (name + numeric status). */\nexport function isPlatformHubErrorLike(error: unknown): error is Error & { status: number; code?: string } {\n return (\n error instanceof Error &&\n error.name === 'PlatformHubError' &&\n typeof (error as { status?: unknown }).status === 'number'\n )\n}\n\n/** Structural subset of the platform hub wire client — extra methods are fine. */\nexport interface HubClientLike {\n catalog(): Promise<unknown>\n listConnections(): Promise<unknown>\n revokeConnection(connectionId: string): Promise<unknown>\n startAuth(input: {\n providerId: string\n connectorId: string\n returnUrl: string\n requestedScopes?: string[]\n }): Promise<{ authorizationUrl: string; state: string }>\n listHealthchecks(): Promise<unknown>\n}\n\n/** Define methods to require user ID, get bearer token, and create a hub client bound to the bearer */\nexport interface HubProxyContext {\n /** Resolve the authenticated user id. Throw the app's own auth Response /\n * redirect to reject — it propagates untouched. */\n requireUserId(request: Request): Promise<string>\n /** The user's platform bearer; throw `TangleBearerMissingError` when unlinked. */\n getBearer(userId: string): Promise<string>\n /** A hub client bound to the bearer. */\n createHubClient(bearer: string): HubClientLike\n}\n\n/** Define arguments for configuring a proxy route with request and optional parameters */\nexport interface HubProxyRouteArgs {\n request: Request\n params?: Record<string, string | undefined>\n}\n\n/** Define routes for hub proxy handling catalog, connections, healthchecks, and authorization actions */\nexport interface HubProxyRoutes {\n /** GET → `{ catalog }`. */\n catalog(args: HubProxyRouteArgs): Promise<Response>\n /** GET → `{ connections }`. */\n connections(args: HubProxyRouteArgs): Promise<Response>\n /** DELETE → the platform revocation result verbatim; 405 otherwise. */\n connectionDelete(args: { request: Request; params: { connectionId: string } }): Promise<Response>\n /** GET → `{ healthchecks }`. */\n healthchecks(args: HubProxyRouteArgs): Promise<Response>\n /** POST `{ providerId, connectorId, returnUrl, requestedScopes? }` →\n * `{ authorizationUrl, state }`; 405 non-POST; 400 on bad JSON / missing fields. */\n authStart(args: HubProxyRouteArgs): Promise<Response>\n}\n\ninterface StartAuthBody {\n providerId?: string\n connectorId?: string\n returnUrl?: string\n requestedScopes?: string[]\n}\n\n/** Resolve hub proxy routes with authentication and error handling based on the given context */\nexport function createHubProxyRoutes(ctx: HubProxyContext): HubProxyRoutes {\n /** Auth runs OUTSIDE the proxy try/catch so the app's auth throw (redirect\n * Response etc.) is never swallowed; bearer + platform errors are mapped. */\n async function proxy(request: Request, call: (hub: HubClientLike) => Promise<Response>): Promise<Response> {\n const userId = await ctx.requireUserId(request)\n try {\n const bearer = await ctx.getBearer(userId)\n return await call(ctx.createHubClient(bearer))\n } catch (err) {\n if (isTangleBearerMissingError(err)) {\n return Response.json({ error: 'tangle_link_required' }, { status: 412 })\n }\n if (isPlatformHubErrorLike(err)) {\n return Response.json({ error: err.message, code: err.code }, { status: err.status })\n }\n throw err\n }\n }\n\n return {\n catalog: ({ request }) => proxy(request, async (hub) => Response.json({ catalog: await hub.catalog() })),\n\n connections: ({ request }) =>\n proxy(request, async (hub) => Response.json({ connections: await hub.listConnections() })),\n\n connectionDelete: async ({ request, params }) => {\n if (request.method !== 'DELETE') {\n return Response.json({ error: 'Method not allowed' }, { status: 405 })\n }\n return proxy(request, async (hub) => Response.json(await hub.revokeConnection(params.connectionId)))\n },\n\n healthchecks: ({ request }) =>\n proxy(request, async (hub) => Response.json({ healthchecks: await hub.listHealthchecks() })),\n\n authStart: async ({ request }) => {\n if (request.method !== 'POST') {\n return Response.json({ error: 'Method not allowed' }, { status: 405 })\n }\n const userId = await ctx.requireUserId(request)\n let body: StartAuthBody\n try {\n body = (await request.json()) as StartAuthBody\n } catch {\n return Response.json({ error: 'Invalid JSON body' }, { status: 400 })\n }\n if (!body.providerId || !body.connectorId || !body.returnUrl) {\n return Response.json({ error: 'providerId, connectorId, and returnUrl are required' }, { status: 400 })\n }\n try {\n const bearer = await ctx.getBearer(userId)\n const result = await ctx.createHubClient(bearer).startAuth({\n providerId: body.providerId,\n connectorId: body.connectorId,\n returnUrl: body.returnUrl,\n requestedScopes: body.requestedScopes,\n })\n return Response.json({ authorizationUrl: result.authorizationUrl, state: result.state })\n } catch (err) {\n if (isTangleBearerMissingError(err)) {\n return Response.json({ error: 'tangle_link_required' }, { status: 412 })\n }\n if (isPlatformHubErrorLike(err)) {\n return Response.json({ error: err.message, code: err.code }, { status: err.status })\n }\n throw err\n }\n },\n }\n}\n","/**\n * Platform billing HTTP transport + tier state for apps on the shared\n * Tangle balance model (id.tangle.tools). Reads authenticate as the user via\n * their per-user platform key (the platform resolves the caller from the\n * key; service or impersonation headers on read routes are rejected). The\n * deduct write authenticates as the product service (`Bearer <serviceToken>`\n * + `X-Service-Name`) and names the target user in the body. Also provides a\n * fetch-backed implementation of the `/billing` module's\n * `PlatformBillingClient` seam (type-only import — no runtime coupling).\n */\n\nimport type { PlatformBillingClient, PlatformIdentity } from '../billing/index'\nimport { resolveTangleExecutionEnvironment } from '../runtime/model'\n\n/** Define available subscription tiers for the TanglePlan service */\nexport type TanglePlanTier = 'free' | 'pro' | 'enterprise'\n\n/** 'pro' | 'enterprise' pass through; anything else (null, unknown) → 'free'. */\nexport function normalizeTanglePlanTier(plan: string | null | undefined): TanglePlanTier {\n return plan === 'pro' || plan === 'enterprise' ? plan : 'free'\n}\n\n/** Represent platform billing HTTP errors with status code and detailed message */\nexport class PlatformBillingHttpError extends Error {\n constructor(\n readonly status: number,\n detail: string,\n ) {\n super(`Platform request failed (${status}): ${detail}`)\n this.name = 'PlatformBillingHttpError'\n }\n}\n\n/** Structural guard (name + numeric status) — robust across module instances. */\nexport function isPlatformBillingHttpError(error: unknown): error is PlatformBillingHttpError {\n return (\n error instanceof Error &&\n error.name === 'PlatformBillingHttpError' &&\n typeof (error as { status?: unknown }).status === 'number'\n )\n}\n\n/** Define HTTP options for platform billing including base URL, service token, product slug, fetch implementation, and timeout */\nexport interface PlatformBillingHttpOptions {\n /** Platform root, e.g. https://id.tangle.tools (trailing slashes stripped). */\n baseUrl: string\n /** Used only by `deduct()`; resolved lazily so reads never require it.\n * Throws at call time when empty. */\n serviceToken: string | (() => string)\n /** Product slug — the `X-Service-Name` header and the deduct `product` field. */\n productSlug: string\n fetchImpl?: typeof fetch\n /** Default 10 000. */\n timeoutMs?: number\n}\n\n/** Describe subscription tier and status information for a platform user */\nexport interface PlatformSubscriptionInfo {\n tier: TanglePlanTier\n status: string | null\n}\n\n/** Describe the platform balance and lifetime spending with an optional update timestamp */\nexport interface PlatformBalanceSnapshot {\n balance: number\n lifetimeSpent: number\n updatedAt?: string\n}\n\n/** Describe a product's usage and spending metrics on the platform */\nexport interface PlatformUsageProductRow {\n product: string | null\n totalSpent: number\n count: number\n}\n\n/** Lifecycle of a per-product seat subscription, mirroring the Stripe states\n * the platform persists. 'none' = the user has never held this seat. */\nexport type SeatStatus = 'none' | 'active' | 'trialing' | 'past_due' | 'canceled'\n\n/** Price and included shared-wallet credit for one seat billing period. */\nexport interface ProductSeatOfferPeriod {\n priceCents: number\n includedCreditsCents: number\n}\n\n/** Commercial terms returned by the platform's product catalog. Products use\n * this exact object for display instead of duplicating prices in UI copy. */\nexport interface ProductSeatOffer {\n currency: 'usd'\n interval: 'month'\n recurring: ProductSeatOfferPeriod\n introductory: ProductSeatOfferPeriod | null\n}\n\n/**\n * Per-product entitlement snapshot from the platform — the single read that\n * tells a product whether to show its workspace or the seat paywall. Shape\n * matches `GET /v1/billing/product-entitlement?product=<id>`.\n *\n * `hasSeat` is computed platform-side from the raw seat row.\n * It is true for an active or trialing seat whose period has not lapsed.\n * `onFreeTier` remains in the wire shape for compatibility, but this client\n * always returns false and never uses it for access.\n */\nexport interface ProductEntitlement {\n seatStatus: SeatStatus\n /** ISO timestamp the active seat's paid period runs until; null when none. */\n currentPeriodEnd: string | null\n /** Cumulative inference spend across the whole suite, in dollars. */\n lifetimeSpentUsd: number\n hasSeat: boolean\n onFreeTier: boolean\n /** Present when the platform exposes catalog-backed commercial terms. */\n offer?: ProductSeatOffer\n}\n\nfunction productSeatOffer(value: unknown): ProductSeatOffer | undefined {\n if (!value || typeof value !== 'object') return undefined\n const candidate = value as Record<string, unknown>\n if (candidate.currency !== 'usd' || candidate.interval !== 'month') return undefined\n\n const period = (input: unknown, allowZeroPrice: boolean): ProductSeatOfferPeriod | undefined => {\n if (!input || typeof input !== 'object') return undefined\n const data = input as Record<string, unknown>\n const priceCents = data.priceCents\n const includedCreditsCents = data.includedCreditsCents\n if (\n typeof priceCents !== 'number' ||\n !Number.isSafeInteger(priceCents) ||\n (allowZeroPrice ? priceCents < 0 : priceCents <= 0) ||\n typeof includedCreditsCents !== 'number' ||\n !Number.isSafeInteger(includedCreditsCents) ||\n includedCreditsCents < 0\n ) {\n return undefined\n }\n return { priceCents, includedCreditsCents }\n }\n\n const recurring = period(candidate.recurring, false)\n const introductory =\n candidate.introductory === null\n ? null\n : period(candidate.introductory, true)\n if (!recurring || introductory === undefined) return undefined\n return {\n currency: 'usd',\n interval: 'month',\n recurring,\n introductory,\n }\n}\n\n/** Define methods to interact with platform billing endpoints using user or service authentication */\nexport interface PlatformBillingHttp {\n /** GET /v1/plans/current (user bearer). */\n getSubscription(userApiKey: string): Promise<PlatformSubscriptionInfo>\n /** GET /v1/billing/balance (user bearer). */\n getBalance(userApiKey: string): Promise<PlatformBalanceSnapshot>\n /** GET /v1/billing/usage (user bearer). */\n getUsageByProduct(userApiKey: string): Promise<PlatformUsageProductRow[]>\n /** GET /v1/billing/product-entitlement?product=<id> (user bearer). */\n getProductEntitlement(userApiKey: string, productId: string): Promise<ProductEntitlement>\n /** POST /v1/billing/deduct (service token). */\n deduct(input: {\n platformUserId: string\n amountUsd: number\n type: string\n description: string\n referenceId: string\n }): Promise<void>\n /** Absolute URL of the platform's billing-management surface. */\n billingUrl(): string\n /** Absolute URL of the catalog-backed seat checkout for `productId`. */\n seatCheckoutUrl(productId: string): string\n}\n\n/** Create a PlatformBillingHttp instance configured with given options and default behaviors */\nexport function createPlatformBillingHttp(opts: PlatformBillingHttpOptions): PlatformBillingHttp {\n const baseUrl = opts.baseUrl.replace(/\\/+$/, '')\n if (!baseUrl) throw new Error('PlatformBillingHttpOptions.baseUrl is required')\n if (!opts.productSlug) throw new Error('PlatformBillingHttpOptions.productSlug is required')\n const fetchImpl = opts.fetchImpl ?? fetch\n const timeoutMs = opts.timeoutMs ?? 10_000\n\n function resolveServiceToken(): string {\n const token = typeof opts.serviceToken === 'function' ? opts.serviceToken() : opts.serviceToken\n if (!token) throw new Error('A platform service token is required for deduct')\n return token\n }\n\n async function request<T>(path: string, init: RequestInit, headers: Headers): Promise<T> {\n const res = await fetchImpl(`${baseUrl}${path}`, {\n ...init,\n headers,\n signal: AbortSignal.timeout(timeoutMs),\n })\n if (!res.ok) {\n const body = (await res.json().catch(() => null)) as { error?: { message?: string } } | null\n throw new PlatformBillingHttpError(res.status, body?.error?.message ?? res.statusText)\n }\n return res.json() as Promise<T>\n }\n\n function userRead<T>(userApiKey: string, path: string): Promise<T> {\n const headers = new Headers()\n headers.set('Authorization', `Bearer ${userApiKey}`)\n return request<T>(path, {}, headers)\n }\n\n return {\n async getSubscription(userApiKey) {\n const body = await userRead<{\n success: boolean\n data?: { subscription?: { plan?: string | null; status?: string | null } | null }\n }>(userApiKey, '/v1/plans/current')\n const sub = body.data?.subscription ?? null\n return { tier: normalizeTanglePlanTier(sub?.plan), status: sub?.status ?? null }\n },\n\n async getBalance(userApiKey) {\n const body = await userRead<{\n success: boolean\n data?: { balance?: number; lifetimeSpent?: number; updatedAt?: string }\n }>(userApiKey, '/v1/billing/balance')\n return {\n balance: body.data?.balance ?? 0,\n lifetimeSpent: body.data?.lifetimeSpent ?? 0,\n updatedAt: body.data?.updatedAt,\n }\n },\n\n async getUsageByProduct(userApiKey) {\n const body = await userRead<{\n success: boolean\n data?: Array<{ product?: string | null; totalSpent?: number; count?: number }>\n }>(userApiKey, '/v1/billing/usage')\n return (body.data ?? []).map((row) => ({\n product: row.product ?? null,\n totalSpent: row.totalSpent ?? 0,\n count: row.count ?? 0,\n }))\n },\n\n async getProductEntitlement(userApiKey, productId) {\n const slug = encodeURIComponent(productId)\n const body = await userRead<{\n success: boolean\n data?: {\n seatStatus?: SeatStatus | null\n currentPeriodEnd?: string | null\n lifetimeSpentUsd?: number | null\n hasSeat?: boolean | null\n onFreeTier?: boolean | null\n offer?: unknown\n }\n }>(userApiKey, `/v1/billing/product-entitlement?product=${slug}`)\n const data = body.data ?? {}\n const hasSeat = data.hasSeat === true\n const offer = productSeatOffer(data.offer)\n return {\n seatStatus: data.seatStatus ?? 'none',\n currentPeriodEnd: data.currentPeriodEnd ?? null,\n lifetimeSpentUsd: data.lifetimeSpentUsd ?? 0,\n hasSeat,\n // Product-funded free access is retired. Ignore stale server signals.\n onFreeTier: false,\n ...(offer ? { offer } : {}),\n }\n },\n\n async deduct(input) {\n const headers = new Headers()\n headers.set('Authorization', `Bearer ${resolveServiceToken()}`)\n headers.set('X-Service-Name', opts.productSlug)\n headers.set('Content-Type', 'application/json')\n await request('/v1/billing/deduct', {\n method: 'POST',\n body: JSON.stringify({\n userId: input.platformUserId,\n amount: input.amountUsd,\n type: input.type,\n product: opts.productSlug,\n description: input.description,\n referenceId: input.referenceId,\n }),\n }, headers)\n },\n\n billingUrl() {\n return `${baseUrl}/app/billing`\n },\n\n seatCheckoutUrl(productId) {\n return seatCheckoutUrl(baseUrl, productId)\n },\n }\n}\n\n/**\n * Platform Stripe checkout URL for a product's catalog-backed seat. The\n * platform resolves the product-specific price and introductory offer from the\n * `product` query param. Mirrors the `billingUrl()` shape — a deterministic\n * platform-rooted URL with no client-side pricing decisions.\n */\nexport function seatCheckoutUrl(baseUrl: string, productId: string): string {\n const root = baseUrl.replace(/\\/+$/, '')\n return `${root}/app/billing/seat/checkout?product=${encodeURIComponent(productId)}`\n}\n\n// ── Tier policy + composed state ────────────────────────────────────────────\n\n/** Define policy settings for concurrency and overage allowance in a tangle tier */\nexport interface TangleTierPolicy {\n concurrency: number\n overageAllowed: boolean\n}\n\n/** Define default concurrency and overage policies for each TanglePlanTier level */\nexport const DEFAULT_TANGLE_TIER_POLICY: Record<TanglePlanTier, TangleTierPolicy> = {\n free: { concurrency: 1, overageAllowed: false },\n pro: { concurrency: Number.POSITIVE_INFINITY, overageAllowed: true },\n enterprise: { concurrency: Number.POSITIVE_INFINITY, overageAllowed: true },\n}\n\n/** Describe the state of a Tangle plan tier including subscription, balance, spending, and concurrency details */\nexport interface TangleTierState {\n tier: TanglePlanTier\n subscriptionStatus: string | null\n remainingBalanceUsd: number\n lifetimeSpentUsd: number\n concurrency: number\n overageAllowed: boolean\n}\n\n/**\n * Read subscription + balance and project them onto the tier policy. A\n * null/absent key fails CLOSED (free tier, zero balance) — a billable run is\n * never started against an unknown balance. Platform errors throw; callers\n * on the billable path choose their posture explicitly.\n */\nexport async function readTangleTierState(\n http: PlatformBillingHttp,\n userApiKey: string | null | undefined,\n policy: Record<TanglePlanTier, TangleTierPolicy> = DEFAULT_TANGLE_TIER_POLICY,\n): Promise<TangleTierState> {\n if (!userApiKey) {\n return {\n tier: 'free',\n subscriptionStatus: null,\n remainingBalanceUsd: 0,\n lifetimeSpentUsd: 0,\n ...policy.free,\n }\n }\n const [subscription, balance] = await Promise.all([\n http.getSubscription(userApiKey),\n http.getBalance(userApiKey),\n ])\n return {\n tier: subscription.tier,\n subscriptionStatus: subscription.status,\n remainingBalanceUsd: balance.balance,\n lifetimeSpentUsd: balance.lifetimeSpent,\n ...policy[subscription.tier],\n }\n}\n\n// ── Per-product seat entitlement ────────────────────────────────────────────\n\n/** Product-funded free inference spend is disabled. */\nexport const FREE_TIER_SPEND_CAP_USD = 0\n\n/**\n * Default name of the per-app feature flag that controls seat billing.\n * Explicit values override the environment default.\n */\nexport const DEFAULT_SEAT_BILLING_ENABLED_ENV_VAR = 'SEAT_BILLING_ENABLED'\n\n/** Define options to configure seat billing flag environment variables and override flag name */\nexport interface SeatBillingFlagOptions {\n env?: Record<string, string | undefined>\n /** Override the flag name; default {@link DEFAULT_SEAT_BILLING_ENABLED_ENV_VAR}. */\n flagEnvVar?: string\n}\n\n/**\n * An explicit flag controls seat billing in every environment.\n * Without a flag, known development and test environments default to OFF.\n * Deployed and unknown environments default to ON.\n */\nexport function isSeatBillingEnabled(opts: SeatBillingFlagOptions = {}): boolean {\n const env =\n opts.env ??\n (typeof process !== 'undefined' ? (process.env as Record<string, string | undefined>) : undefined)\n if (!env) return true\n const flag = env[opts.flagEnvVar ?? DEFAULT_SEAT_BILLING_ENABLED_ENV_VAR]?.trim().toLowerCase()\n if (flag) return !['false', '0', 'off', 'disabled', 'no'].includes(flag)\n const environment = resolveTangleExecutionEnvironment(env)\n return environment !== 'development' && environment !== 'test'\n}\n\n/**\n * Read a user's entitlement for one product.\n * An absent key or disabled flag returns no access.\n * Transport failures propagate so the product can return a retryable error.\n *\n * @param flag — pass {@link isSeatBillingEnabled} (or your own boolean) so the\n * product owns when the gate engages. When false, no network call is made.\n */\nexport async function getProductEntitlement(\n http: Pick<PlatformBillingHttp, 'getProductEntitlement'>,\n userApiKey: string | null | undefined,\n productId: string,\n flag = true,\n): Promise<ProductEntitlement> {\n if (!flag) return unavailableEntitlement()\n if (!userApiKey) return unavailableEntitlement()\n return http.getProductEntitlement(userApiKey, productId)\n}\n\nfunction unavailableEntitlement(): ProductEntitlement {\n return {\n seatStatus: 'none',\n currentPeriodEnd: null,\n lifetimeSpentUsd: 0,\n hasSeat: false,\n onFreeTier: false,\n }\n}\n\n/** Product access requires an active paid or trialing seat. */\nexport function isProductEntitled(ent: ProductEntitlement): boolean {\n return ent.hasSeat\n}\n\n// ── Bridge onto the /billing seam ───────────────────────────────────────────\n\n/** Define a contract for resolving platform identities based on user identifiers */\nexport interface PlatformIdentityStore {\n resolveIdentity(userId: string): Promise<PlatformIdentity | null>\n}\n\n/** Concrete fetch-backed `PlatformBillingClient<TanglePlanTier>` for\n * `createPlatformBalanceManager` (from `/billing`). */\nexport function createTanglePlatformBillingClient(\n http: PlatformBillingHttp,\n identity: PlatformIdentityStore,\n): PlatformBillingClient<TanglePlanTier> {\n return {\n resolveIdentity: (userId) => identity.resolveIdentity(userId),\n getPlan: async (apiKey) => (await http.getSubscription(apiKey)).tier,\n getBalance: async (apiKey) => {\n const snapshot = await http.getBalance(apiKey)\n return { balance: snapshot.balance, lifetimeSpent: snapshot.lifetimeSpent }\n },\n getUsageByProduct: (apiKey) => http.getUsageByProduct(apiKey),\n deduct: (input) => http.deduct(input),\n }\n}\n"],"mappings":"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","names":[]}
|