@ekanos/harness 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +879 -0
  3. package/dist/app.d.ts +4 -0
  4. package/dist/app.js +11 -0
  5. package/dist/config.d.ts +18 -0
  6. package/dist/config.js +34 -0
  7. package/dist/internal/components/ask-assistant-bridge.d.ts +10 -0
  8. package/dist/internal/components/ask-assistant-bridge.js +50 -0
  9. package/dist/internal/components/dashboard-grid.d.ts +45 -0
  10. package/dist/internal/components/dashboard-grid.js +84 -0
  11. package/dist/internal/components/dev-toolbar.d.ts +15 -0
  12. package/dist/internal/components/dev-toolbar.js +155 -0
  13. package/dist/internal/components/harness-providers.d.ts +16 -0
  14. package/dist/internal/components/harness-providers.js +83 -0
  15. package/dist/internal/components/harness-widget-provider.d.ts +60 -0
  16. package/dist/internal/components/harness-widget-provider.js +84 -0
  17. package/dist/internal/components/i18n-provider.d.ts +9 -0
  18. package/dist/internal/components/i18n-provider.js +9 -0
  19. package/dist/internal/components/row-groups.d.ts +23 -0
  20. package/dist/internal/components/row-groups.js +37 -0
  21. package/dist/internal/components/surface-nav.d.ts +4 -0
  22. package/dist/internal/components/surface-nav.js +70 -0
  23. package/dist/internal/components/viewport-frame.d.ts +14 -0
  24. package/dist/internal/components/viewport-frame.js +27 -0
  25. package/dist/internal/components/widget-boundary.d.ts +25 -0
  26. package/dist/internal/components/widget-boundary.js +44 -0
  27. package/dist/internal/components/widget-surface.d.ts +20 -0
  28. package/dist/internal/components/widget-surface.js +76 -0
  29. package/dist/internal/lib/fonts.d.ts +2 -0
  30. package/dist/internal/lib/fonts.js +22 -0
  31. package/dist/internal/lib/harness-fetch-interceptor.d.ts +89 -0
  32. package/dist/internal/lib/harness-fetch-interceptor.js +101 -0
  33. package/dist/internal/lib/harness-live-fetch.d.ts +66 -0
  34. package/dist/internal/lib/harness-live-fetch.js +121 -0
  35. package/dist/internal/lib/harness-query-client.d.ts +43 -0
  36. package/dist/internal/lib/harness-query-client.js +103 -0
  37. package/dist/internal/lib/http-fixtures.d.ts +145 -0
  38. package/dist/internal/lib/http-fixtures.js +256 -0
  39. package/dist/internal/lib/i18n.d.ts +2 -0
  40. package/dist/internal/lib/i18n.js +17 -0
  41. package/dist/internal/lib/redact.d.ts +33 -0
  42. package/dist/internal/lib/redact.js +43 -0
  43. package/dist/internal/lib/toolbar-context.d.ts +59 -0
  44. package/dist/internal/lib/toolbar-context.js +124 -0
  45. package/dist/internal/registry-context.d.ts +27 -0
  46. package/dist/internal/registry-context.js +51 -0
  47. package/dist/internal/routes/activation-page.d.ts +33 -0
  48. package/dist/internal/routes/activation-page.js +242 -0
  49. package/dist/internal/routes/index-page.d.ts +13 -0
  50. package/dist/internal/routes/index-page.js +62 -0
  51. package/dist/internal/routes/integration-layout.d.ts +9 -0
  52. package/dist/internal/routes/integration-layout.js +84 -0
  53. package/dist/internal/routes/root-layout.d.ts +34 -0
  54. package/dist/internal/routes/root-layout.js +39 -0
  55. package/dist/internal/routes/single-widget-page.d.ts +6 -0
  56. package/dist/internal/routes/single-widget-page.js +30 -0
  57. package/dist/internal/routes/tile-page.d.ts +1 -0
  58. package/dist/internal/routes/tile-page.js +88 -0
  59. package/dist/internal/routes/triggers-page.d.ts +1 -0
  60. package/dist/internal/routes/triggers-page.js +386 -0
  61. package/dist/internal/routes/widgets-page.d.ts +1 -0
  62. package/dist/internal/routes/widgets-page.js +19 -0
  63. package/dist/internal/surfaces.d.ts +25 -0
  64. package/dist/internal/surfaces.js +29 -0
  65. package/dist/mocks/team-account-workspace.d.ts +78 -0
  66. package/dist/mocks/team-account-workspace.js +86 -0
  67. package/dist/registry.d.ts +418 -0
  68. package/dist/registry.js +82 -0
  69. package/dist/routes.d.ts +24 -0
  70. package/dist/routes.js +15 -0
  71. package/dist/styles.css +236 -0
  72. package/package.json +101 -0
@@ -0,0 +1,386 @@
1
+ "use client";
2
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
3
+ import { useState } from "react";
4
+ import { notFound } from "next/navigation";
5
+ import { isEgressAllowed } from "@ekanos/sdk/context";
6
+ import {
7
+ createMockContext,
8
+ invokeSchedule,
9
+ invokeWebhook
10
+ } from "@ekanos/sdk/testing";
11
+ import { Alert, AlertDescription, AlertTitle } from "@ekanos/ui/alert";
12
+ import { Badge } from "@ekanos/ui/badge";
13
+ import { Button } from "@ekanos/ui/button";
14
+ import { Textarea } from "@ekanos/ui/textarea";
15
+ import {
16
+ HARNESS_ACCOUNT_ID,
17
+ resolveEgress
18
+ } from "../../registry.js";
19
+ import { compileFixtures, fixturesToMockHandlers } from "../lib/http-fixtures.js";
20
+ import { useToolbar } from "../lib/toolbar-context.js";
21
+ import { useHarnessIntegration } from "../registry-context.js";
22
+ function useTriggerContext(integration, variant) {
23
+ const [ctx] = useState(() => {
24
+ const definition = integration.definition;
25
+ if (!definition) return null;
26
+ const mocks = integration.triggerMocks ?? {};
27
+ return createMockContext({
28
+ accountId: HARNESS_ACCOUNT_ID,
29
+ integration: { slug: definition.slug },
30
+ ...definition.storage ? { storageSchemas: definition.storage } : {},
31
+ egress: definition.egress ?? [],
32
+ ...mocks.storage ? {
33
+ storage: mocks.storage
34
+ } : {},
35
+ ...mocks.secrets ? { secrets: mocks.secrets } : {},
36
+ fetchHandlers: [
37
+ ...mocks.fetchHandlers ?? [],
38
+ ...fixturesToMockHandlers(
39
+ // Client-only, like the layout: no origin on the server, and
40
+ // nothing fetches there.
41
+ globalThis.location === void 0 ? [] : compileFixtures(
42
+ integration.fixtures?.[variant] ?? [],
43
+ resolveEgress(integration),
44
+ globalThis.location.origin
45
+ )
46
+ )
47
+ ]
48
+ });
49
+ });
50
+ return ctx;
51
+ }
52
+ function OutcomePanel({ outcome }) {
53
+ return /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-3", "data-test": "trigger-outcome", children: [
54
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
55
+ /* @__PURE__ */ jsx(Badge, { variant: outcome.ok ? "default" : "destructive", children: outcome.summary }),
56
+ outcome.detail ? /* @__PURE__ */ jsx("span", { className: "text-muted-foreground text-sm", children: outcome.detail }) : null
57
+ ] }),
58
+ outcome.logs.length > 0 ? /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-1", children: [
59
+ /* @__PURE__ */ jsx("span", { className: "text-muted-foreground text-xs", children: "Handler logs" }),
60
+ /* @__PURE__ */ jsx("pre", { className: "bg-muted max-h-48 overflow-auto rounded-md p-3 text-xs", children: outcome.logs.map((log) => `[${log.level}] ${log.message}`).join("\n") })
61
+ ] }) : null,
62
+ /* @__PURE__ */ jsx("div", { className: "flex flex-col gap-3", children: ["account", "user"].map((scope) => /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-1", children: [
63
+ /* @__PURE__ */ jsxs("span", { className: "text-muted-foreground text-xs", children: [
64
+ scope === "account" ? "Account" : "User",
65
+ " storage after this run",
66
+ Object.keys(outcome.storage[scope]).length === 0 ? " (empty)" : ""
67
+ ] }),
68
+ /* @__PURE__ */ jsx("pre", { className: "bg-muted max-h-64 overflow-auto rounded-md p-3 text-xs", children: JSON.stringify(outcome.storage[scope], null, 2) })
69
+ ] }, scope)) })
70
+ ] });
71
+ }
72
+ function describeError(error) {
73
+ return error instanceof Error ? error.message : String(error);
74
+ }
75
+ function WebhookCard({
76
+ definition,
77
+ webhook,
78
+ ctx
79
+ }) {
80
+ const [state, setState] = useState(() => ({
81
+ payload: JSON.stringify(webhook.examplePayload ?? {}, null, 2),
82
+ outcome: null,
83
+ busy: false
84
+ }));
85
+ const deliver = async () => {
86
+ setState((s) => ({ ...s, busy: true }));
87
+ const logsBefore = ctx.logs.length;
88
+ let outcome;
89
+ try {
90
+ const payload = JSON.parse(state.payload);
91
+ const { result } = await invokeWebhook(definition, webhook.id, payload, {
92
+ context: ctx,
93
+ headers: webhook.signature === "none" ? {} : { [webhook.signature.header]: "(unverified \u2014 local transport)" }
94
+ });
95
+ outcome = {
96
+ ok: true,
97
+ summary: result.status,
98
+ ...result.detail ? { detail: result.detail } : {},
99
+ logs: ctx.logs.slice(logsBefore),
100
+ storage: ctx.dumpStorage()
101
+ };
102
+ } catch (error) {
103
+ console.error("[harness] webhook invocation failed:", error);
104
+ outcome = {
105
+ ok: false,
106
+ summary: "failed",
107
+ detail: describeError(error),
108
+ logs: ctx.logs.slice(logsBefore),
109
+ storage: ctx.dumpStorage()
110
+ };
111
+ }
112
+ setState((s) => ({ ...s, outcome, busy: false }));
113
+ };
114
+ return /* @__PURE__ */ jsxs(
115
+ "div",
116
+ {
117
+ className: "bg-card flex flex-col gap-5 rounded-lg border p-6",
118
+ "data-test": `webhook-${webhook.id}`,
119
+ children: [
120
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center gap-3", children: [
121
+ /* @__PURE__ */ jsx("h3", { className: "font-heading text-lg tracking-tight", children: /* @__PURE__ */ jsx("code", { children: webhook.id }) }),
122
+ /* @__PURE__ */ jsx(Badge, { variant: "outline", children: "webhook" }),
123
+ webhook.signature === "none" ? /* @__PURE__ */ jsx(Badge, { variant: "secondary", children: "unsigned" }) : /* @__PURE__ */ jsxs(Badge, { variant: "secondary", children: [
124
+ "signed: ",
125
+ webhook.signature.header,
126
+ ' \u21D0 secret "',
127
+ webhook.signature.secretName,
128
+ '"'
129
+ ] })
130
+ ] }),
131
+ /* @__PURE__ */ jsx("p", { className: "text-muted-foreground text-sm", children: webhook.description }),
132
+ webhook.signature !== "none" ? /* @__PURE__ */ jsx("p", { className: "text-muted-foreground text-xs", children: "Signature verification is the transport's job \u2014 the harness skips it (and says so in the handler logs); the host ingress will enforce it before your handler ever runs." }) : null,
133
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-2", children: [
134
+ /* @__PURE__ */ jsxs("span", { className: "text-muted-foreground text-xs", children: [
135
+ "Payload (validated against ",
136
+ /* @__PURE__ */ jsx("code", { children: "payloadSchema" }),
137
+ " before the handler runs)"
138
+ ] }),
139
+ /* @__PURE__ */ jsx(
140
+ Textarea,
141
+ {
142
+ value: state.payload,
143
+ onChange: (event) => setState((s) => ({ ...s, payload: event.target.value })),
144
+ rows: 10,
145
+ className: "font-mono text-xs",
146
+ "data-test": `webhook-payload-${webhook.id}`
147
+ }
148
+ )
149
+ ] }),
150
+ /* @__PURE__ */ jsx("div", { children: /* @__PURE__ */ jsx(
151
+ Button,
152
+ {
153
+ onClick: deliver,
154
+ disabled: state.busy,
155
+ "data-test": `webhook-deliver-${webhook.id}`,
156
+ children: "Deliver webhook"
157
+ }
158
+ ) }),
159
+ state.outcome ? /* @__PURE__ */ jsx(OutcomePanel, { outcome: state.outcome }) : null
160
+ ]
161
+ }
162
+ );
163
+ }
164
+ function ScheduleCard({
165
+ definition,
166
+ schedule,
167
+ ctx
168
+ }) {
169
+ const [state, setState] = useState({ outcome: null, busy: false });
170
+ const runNow = async () => {
171
+ setState((s) => ({ ...s, busy: true }));
172
+ const logsBefore = ctx.logs.length;
173
+ let outcome;
174
+ try {
175
+ const { result } = await invokeSchedule(definition, schedule.id, {
176
+ context: ctx
177
+ });
178
+ outcome = {
179
+ ok: true,
180
+ summary: result.status,
181
+ ...result.detail ? { detail: result.detail } : {},
182
+ logs: ctx.logs.slice(logsBefore),
183
+ storage: ctx.dumpStorage()
184
+ };
185
+ } catch (error) {
186
+ console.error("[harness] schedule invocation failed:", error);
187
+ outcome = {
188
+ ok: false,
189
+ summary: "failed",
190
+ detail: describeError(error),
191
+ logs: ctx.logs.slice(logsBefore),
192
+ storage: ctx.dumpStorage()
193
+ };
194
+ }
195
+ setState({ outcome, busy: false });
196
+ };
197
+ return /* @__PURE__ */ jsxs(
198
+ "div",
199
+ {
200
+ className: "bg-card flex flex-col gap-5 rounded-lg border p-6",
201
+ "data-test": `schedule-${schedule.id}`,
202
+ children: [
203
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center gap-3", children: [
204
+ /* @__PURE__ */ jsx("h3", { className: "font-heading text-lg tracking-tight", children: /* @__PURE__ */ jsx("code", { children: schedule.id }) }),
205
+ /* @__PURE__ */ jsx(Badge, { variant: "outline", children: "schedule" }),
206
+ /* @__PURE__ */ jsx(Badge, { variant: "secondary", children: /* @__PURE__ */ jsx("code", { children: schedule.cron }) })
207
+ ] }),
208
+ /* @__PURE__ */ jsx("p", { className: "text-muted-foreground text-sm", children: schedule.description }),
209
+ /* @__PURE__ */ jsxs("p", { className: "text-muted-foreground text-xs", children: [
210
+ "The host scheduler fires this cron with",
211
+ " ",
212
+ /* @__PURE__ */ jsx("code", { children: "trigger: 'schedule'" }),
213
+ '. "Run now" invokes the same handler with ',
214
+ /* @__PURE__ */ jsx("code", { children: "trigger: 'manual'" }),
215
+ " \u2014 your handler must be safe under both."
216
+ ] }),
217
+ /* @__PURE__ */ jsx("div", { children: /* @__PURE__ */ jsx(
218
+ Button,
219
+ {
220
+ onClick: runNow,
221
+ disabled: state.busy,
222
+ "data-test": `schedule-run-${schedule.id}`,
223
+ children: "Run now"
224
+ }
225
+ ) }),
226
+ state.outcome ? /* @__PURE__ */ jsx(OutcomePanel, { outcome: state.outcome }) : null
227
+ ]
228
+ }
229
+ );
230
+ }
231
+ function OAuthCard({ definition }) {
232
+ const oauth = definition.oauth;
233
+ if (!oauth) return null;
234
+ const egress = definition.egress ?? [];
235
+ const rows = [
236
+ ["Authorization URL", oauth.provider.authorizationUrl],
237
+ ["Token URL", oauth.provider.tokenUrl],
238
+ ["Scopes", oauth.provider.scopes.join(" ") || "(none)"],
239
+ ["PKCE", oauth.provider.pkce ? "yes" : "no"],
240
+ ["Client id secret", oauth.credentials.clientIdSecretName],
241
+ ["Client secret secret", oauth.credentials.clientSecretSecretName]
242
+ ];
243
+ return /* @__PURE__ */ jsxs(
244
+ "div",
245
+ {
246
+ className: "bg-card flex flex-col gap-5 rounded-lg border p-6",
247
+ "data-test": "oauth-declaration",
248
+ children: [
249
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center gap-3", children: [
250
+ /* @__PURE__ */ jsx("h3", { className: "font-heading text-lg tracking-tight", children: "OAuth declaration" }),
251
+ /* @__PURE__ */ jsx(Badge, { variant: "outline", children: "oauth" })
252
+ ] }),
253
+ /* @__PURE__ */ jsxs("dl", { className: "grid grid-cols-1 gap-x-6 gap-y-2 text-sm sm:grid-cols-[auto_1fr]", children: [
254
+ rows.map(([label, value]) => /* @__PURE__ */ jsxs("div", { className: "contents", children: [
255
+ /* @__PURE__ */ jsx("dt", { className: "text-muted-foreground", children: label }),
256
+ /* @__PURE__ */ jsx("dd", { children: /* @__PURE__ */ jsx("code", { className: "break-all", children: value }) })
257
+ ] }, label)),
258
+ /* @__PURE__ */ jsxs("div", { className: "contents", children: [
259
+ /* @__PURE__ */ jsx("dt", { className: "text-muted-foreground", children: "Egress coverage" }),
260
+ /* @__PURE__ */ jsx("dd", { className: "flex gap-2", children: [
261
+ ...new Map(
262
+ [oauth.provider.authorizationUrl, oauth.provider.tokenUrl].map(
263
+ (url) => [new URL(url).origin, url]
264
+ )
265
+ )
266
+ ].map(([origin]) => {
267
+ const covered = [
268
+ oauth.provider.authorizationUrl,
269
+ oauth.provider.tokenUrl
270
+ ].filter((url) => new URL(url).origin === origin).every((url) => isEgressAllowed(url, egress));
271
+ return /* @__PURE__ */ jsx(
272
+ Badge,
273
+ {
274
+ variant: covered ? "secondary" : "destructive",
275
+ children: origin
276
+ },
277
+ origin
278
+ );
279
+ }) })
280
+ ] })
281
+ ] }),
282
+ /* @__PURE__ */ jsxs(Alert, { children: [
283
+ /* @__PURE__ */ jsx(AlertTitle, { children: "Declaration only \u2014 the flow is the transport" }),
284
+ /* @__PURE__ */ jsxs(AlertDescription, { children: [
285
+ /* @__PURE__ */ jsxs("p", { children: [
286
+ "The authorize \u2192 callback \u2192 code-exchange loop is owned by the transport, not by integration code: your package declares the endpoints and persists tokens in ",
287
+ /* @__PURE__ */ jsx("code", { children: "onTokens" }),
288
+ " via",
289
+ " ",
290
+ /* @__PURE__ */ jsx("code", { children: "ctx.secrets" }),
291
+ ". This harness renders the declaration and checks its egress coverage; it does not run the flow."
292
+ ] }),
293
+ /* @__PURE__ */ jsxs("p", { children: [
294
+ /* @__PURE__ */ jsx("strong", { children: "Redirect URI \u2014 host-derived, and not something you declare." }),
295
+ " ",
296
+ "The host owns the callback route and builds the URI from its own origin, so there is no field for it in",
297
+ " ",
298
+ /* @__PURE__ */ jsx("code", { children: "defineIntegration()" }),
299
+ " and adding one would either be ignored or point the host's callback somewhere it should not go. Register this with your provider, against whichever host will run the flow:"
300
+ ] }),
301
+ /* @__PURE__ */ jsx("p", { children: /* @__PURE__ */ jsxs("code", { children: [
302
+ "https://<your-fusion-host>/api/integrations/",
303
+ definition.slug,
304
+ "/callback"
305
+ ] }) }),
306
+ /* @__PURE__ */ jsxs("p", { children: [
307
+ "The harness does not serve that route, so you cannot complete a round trip here. What you CAN check is above: that the endpoints you declared are inside your egress list. Until a transport exists, unit test ",
308
+ /* @__PURE__ */ jsx("code", { children: "onTokens" }),
309
+ " directly with",
310
+ " ",
311
+ /* @__PURE__ */ jsx("code", { children: "createMockContext()" }),
312
+ " \u2014 it is an ordinary async function."
313
+ ] })
314
+ ] })
315
+ ] })
316
+ ]
317
+ }
318
+ );
319
+ }
320
+ function TriggersPage() {
321
+ const integration = useHarnessIntegration();
322
+ const { state } = useToolbar();
323
+ const ctx = useTriggerContext(
324
+ integration ?? { slug: "", name: "", description: "", widgets: [] },
325
+ state.variant
326
+ );
327
+ if (!integration) notFound();
328
+ const definition = integration.definition;
329
+ const webhooks = definition?.webhooks ?? [];
330
+ const schedules = definition?.schedules ?? [];
331
+ const declaresAnything = webhooks.length > 0 || schedules.length > 0 || Boolean(definition?.oauth);
332
+ return /* @__PURE__ */ jsxs(
333
+ "main",
334
+ {
335
+ className: "mx-auto flex w-full max-w-3xl flex-col gap-6 px-6 py-8",
336
+ "data-test": "triggers",
337
+ children: [
338
+ /* @__PURE__ */ jsxs(Alert, { children: [
339
+ /* @__PURE__ */ jsx(AlertTitle, { children: "The local transport for declared event surfaces" }),
340
+ /* @__PURE__ */ jsx(AlertDescription, { children: /* @__PURE__ */ jsxs("p", { children: [
341
+ "Webhooks and schedules below run their REAL handlers against one in-memory mock context (state accumulates until you leave the page). Payloads are validated against each webhook's",
342
+ " ",
343
+ /* @__PURE__ */ jsx("code", { children: "payloadSchema" }),
344
+ ", signature checks are logged as skipped, and egress is enforced from the definition \u2014 the same declarations the host's ingress, scheduler, and hosted OAuth callback bind to later."
345
+ ] }) })
346
+ ] }),
347
+ !definition ? /* @__PURE__ */ jsxs("p", { className: "text-muted-foreground text-sm", children: [
348
+ "This harness entry does not link its ",
349
+ /* @__PURE__ */ jsx("code", { children: "defineIntegration()" }),
350
+ " ",
351
+ "output. Add ",
352
+ /* @__PURE__ */ jsx("code", { children: "definition: integration" }),
353
+ " to the entry in your harness config to light this surface up."
354
+ ] }) : !declaresAnything ? /* @__PURE__ */ jsxs("p", { className: "text-muted-foreground text-sm", children: [
355
+ /* @__PURE__ */ jsx("code", { children: definition.slug }),
356
+ " declares no webhooks, schedules, or OAuth \u2014 which is a perfectly good answer for an integration that only reads. The ",
357
+ /* @__PURE__ */ jsx("code", { children: "@ekanos/sdk" }),
358
+ " README has a worked example of each, under \u201CDeclarable surfaces\u201D."
359
+ ] }) : /* @__PURE__ */ jsxs(Fragment, { children: [
360
+ ctx ? webhooks.map((webhook) => /* @__PURE__ */ jsx(
361
+ WebhookCard,
362
+ {
363
+ definition,
364
+ webhook,
365
+ ctx
366
+ },
367
+ webhook.id
368
+ )) : null,
369
+ ctx ? schedules.map((schedule) => /* @__PURE__ */ jsx(
370
+ ScheduleCard,
371
+ {
372
+ definition,
373
+ schedule,
374
+ ctx
375
+ },
376
+ schedule.id
377
+ )) : null,
378
+ /* @__PURE__ */ jsx(OAuthCard, { definition })
379
+ ] })
380
+ ]
381
+ }
382
+ );
383
+ }
384
+ export {
385
+ TriggersPage
386
+ };
@@ -0,0 +1 @@
1
+ export declare function WidgetsPage(): import("react").JSX.Element;
@@ -0,0 +1,19 @@
1
+ "use client";
2
+ import { jsx } from "react/jsx-runtime";
3
+ import { notFound } from "next/navigation";
4
+ import { WidgetSurface } from "../components/widget-surface.js";
5
+ import { useHarnessIntegration } from "../registry-context.js";
6
+ function WidgetsPage() {
7
+ const integration = useHarnessIntegration();
8
+ if (!integration) notFound();
9
+ return /* @__PURE__ */ jsx("main", { className: "mx-auto w-full max-w-5xl px-6 py-8", "data-test": "widgets", children: integration.widgets.length === 0 ? /* @__PURE__ */ jsx("p", { className: "text-muted-foreground text-sm", children: "This integration registers no widgets." }) : /* @__PURE__ */ jsx(
10
+ WidgetSurface,
11
+ {
12
+ widgets: integration.widgets,
13
+ productSlug: integration.slug
14
+ }
15
+ ) });
16
+ }
17
+ export {
18
+ WidgetsPage
19
+ };
@@ -0,0 +1,25 @@
1
+ /**
2
+ * The per-integration surfaces, in tab order — declared ONCE.
3
+ *
4
+ * This list used to exist twice: `surface-nav.tsx` had four entries and
5
+ * `index-page.tsx` had three, and the one the index dropped was Triggers. A
6
+ * partner dogfooding the harness called Triggers the best surface in the
7
+ * product and found it only by reading our clean-room script, because nothing
8
+ * on the landing page linked to it.
9
+ *
10
+ * Two lists of the same thing drift silently; a missing tab is invisible until
11
+ * someone counts. Both consumers now read this, so adding a surface is one
12
+ * edit. No `'use client'` — the index page is a server component and must
13
+ * stay one.
14
+ */
15
+ export interface HarnessSurface {
16
+ /** URL segment under `/[slug]/`. */
17
+ segment: string;
18
+ /** Tab-bar label. */
19
+ label: string;
20
+ /** Shorter label for the landing page's link row, where space is tight. */
21
+ shortLabel: string;
22
+ /** Font Awesome spec — a weight the FREE package actually ships. */
23
+ icon: string;
24
+ }
25
+ export declare const HARNESS_SURFACES: readonly HarnessSurface[];
@@ -0,0 +1,29 @@
1
+ const HARNESS_SURFACES = [
2
+ {
3
+ segment: "widgets",
4
+ label: "Widgets",
5
+ shortLabel: "Widgets",
6
+ icon: "fa-solid fa-table-cells-large"
7
+ },
8
+ {
9
+ segment: "tile",
10
+ label: "Marketplace tile",
11
+ shortLabel: "Tile",
12
+ icon: "fa-solid fa-store"
13
+ },
14
+ {
15
+ segment: "activation",
16
+ label: "Activation",
17
+ shortLabel: "Activation",
18
+ icon: "fa-solid fa-plug"
19
+ },
20
+ {
21
+ segment: "triggers",
22
+ label: "Triggers",
23
+ shortLabel: "Triggers",
24
+ icon: "fa-solid fa-bolt"
25
+ }
26
+ ];
27
+ export {
28
+ HARNESS_SURFACES
29
+ };
@@ -0,0 +1,78 @@
1
+ import { type ReactNode } from 'react';
2
+ /**
3
+ * A drop-in mock of the host's `use-team-account-workspace` hook.
4
+ *
5
+ * WHY THIS EXISTS: most first-party widgets call `useTeamAccountWorkspace()`
6
+ * for `account.slug` when building navigation links. Importing the genuine hook
7
+ * would be fine on its own, but it re-exports through the host's team-accounts
8
+ * component barrel, which reaches Supabase — and the harness is meant to have
9
+ * no Supabase in it at all.
10
+ *
11
+ * So the harness aliases that specifier onto this module. The specifier itself
12
+ * is named ONLY in build config — `next.config.mjs` →
13
+ * `turbopack.resolveAlias`, and the matching `paths` entry in `tsconfig.json` —
14
+ * never in harness source, which imports nothing from the host workspace.
15
+ * Unmodified first-party widget source keeps compiling and running; it just
16
+ * gets harness values. Partner integrations built on `@ekanos/sdk` never reach
17
+ * for the hook at all (neither the SDK nor either example imports it), so a
18
+ * scaffolded copy can drop the alias and this file together.
19
+ *
20
+ * Two behaviours of the real hook are reproduced deliberately:
21
+ * - the context holds a PROMISE, not a resolved value;
22
+ * - the hook calls `use()` on it, so it suspends.
23
+ * The promise below is created once at module scope and already resolved, so
24
+ * `use()` settles on the first attempt and nothing visibly suspends — but a
25
+ * `<Suspense>` boundary above it is still the correct thing to have.
26
+ */
27
+ export interface HarnessAccount {
28
+ id: string;
29
+ name: string;
30
+ permissions: string[];
31
+ picture_url: string;
32
+ primary_owner_user_id: string;
33
+ role: string;
34
+ role_hierarchy_level: number;
35
+ slug: string;
36
+ source_id: string;
37
+ subscription_status: string;
38
+ }
39
+ export interface HarnessUser {
40
+ id: string;
41
+ email: string;
42
+ phone: string;
43
+ is_anonymous: boolean;
44
+ aal: 'aal1' | 'aal2';
45
+ app_metadata: Record<string, unknown>;
46
+ user_metadata: Record<string, unknown>;
47
+ amr: unknown[];
48
+ }
49
+ export interface AccountWorkspace {
50
+ accounts: {
51
+ id: string | null;
52
+ name: string | null;
53
+ picture_url: string | null;
54
+ role: string | null;
55
+ slug: string | null;
56
+ source_id: string | null;
57
+ }[];
58
+ account: HarnessAccount;
59
+ user: HarnessUser;
60
+ membership?: {
61
+ preferences: {
62
+ first_use: {
63
+ completed: boolean;
64
+ completed_at: string | null;
65
+ skipped: boolean;
66
+ steps_completed: string[];
67
+ };
68
+ landing_zone: 'dashboard' | 'integrations' | 'workspace' | null;
69
+ };
70
+ };
71
+ }
72
+ export declare const TeamAccountWorkspaceContext: import("react").Context<Promise<AccountWorkspace> | null>;
73
+ export declare function TeamAccountWorkspaceContextProvider({ children, value, }: {
74
+ children: ReactNode;
75
+ value?: Promise<AccountWorkspace>;
76
+ }): import("react").JSX.Element;
77
+ export declare function useTeamAccountWorkspace(): AccountWorkspace;
78
+ export declare function useTeamAccountWorkspaceOptional(): AccountWorkspace | null;
@@ -0,0 +1,86 @@
1
+ "use client";
2
+ import { jsx } from "react/jsx-runtime";
3
+ import { createContext, use, useContext } from "react";
4
+ import {
5
+ HARNESS_ACCOUNT_ID,
6
+ HARNESS_ACCOUNT_SLUG,
7
+ HARNESS_SOURCE_ID
8
+ } from "../registry.js";
9
+ const HARNESS_WORKSPACE = {
10
+ accounts: [
11
+ {
12
+ id: HARNESS_ACCOUNT_ID,
13
+ name: "Harness Team",
14
+ picture_url: null,
15
+ role: "owner",
16
+ slug: HARNESS_ACCOUNT_SLUG,
17
+ source_id: HARNESS_SOURCE_ID
18
+ }
19
+ ],
20
+ account: {
21
+ id: HARNESS_ACCOUNT_ID,
22
+ name: "Harness Team",
23
+ permissions: [
24
+ "roles.manage",
25
+ "billing.manage",
26
+ "settings.manage",
27
+ "members.manage",
28
+ "invites.manage"
29
+ ],
30
+ picture_url: "",
31
+ primary_owner_user_id: "00000000-0000-4000-8000-000000000003",
32
+ role: "owner",
33
+ role_hierarchy_level: 1,
34
+ slug: HARNESS_ACCOUNT_SLUG,
35
+ source_id: HARNESS_SOURCE_ID,
36
+ subscription_status: "active"
37
+ },
38
+ user: {
39
+ id: "00000000-0000-4000-8000-000000000003",
40
+ email: "developer@harness.local",
41
+ phone: "",
42
+ is_anonymous: false,
43
+ aal: "aal1",
44
+ app_metadata: {},
45
+ user_metadata: {},
46
+ amr: []
47
+ },
48
+ membership: {
49
+ preferences: {
50
+ first_use: {
51
+ completed: true,
52
+ completed_at: null,
53
+ skipped: true,
54
+ steps_completed: []
55
+ },
56
+ landing_zone: "dashboard"
57
+ }
58
+ }
59
+ };
60
+ const HARNESS_WORKSPACE_PROMISE = Promise.resolve(HARNESS_WORKSPACE);
61
+ const TeamAccountWorkspaceContext = createContext(HARNESS_WORKSPACE_PROMISE);
62
+ function TeamAccountWorkspaceContextProvider({
63
+ children,
64
+ value = HARNESS_WORKSPACE_PROMISE
65
+ }) {
66
+ return /* @__PURE__ */ jsx(TeamAccountWorkspaceContext.Provider, { value, children });
67
+ }
68
+ function useTeamAccountWorkspace() {
69
+ const ctx = useContext(TeamAccountWorkspaceContext);
70
+ if (!ctx) {
71
+ throw new Error(
72
+ "useTeamAccountWorkspace must be used within a TeamAccountWorkspaceContext.Provider."
73
+ );
74
+ }
75
+ return use(ctx);
76
+ }
77
+ function useTeamAccountWorkspaceOptional() {
78
+ const ctx = useContext(TeamAccountWorkspaceContext);
79
+ return ctx ? use(ctx) : null;
80
+ }
81
+ export {
82
+ TeamAccountWorkspaceContext,
83
+ TeamAccountWorkspaceContextProvider,
84
+ useTeamAccountWorkspace,
85
+ useTeamAccountWorkspaceOptional
86
+ };