@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.
- package/LICENSE +21 -0
- package/README.md +879 -0
- package/dist/app.d.ts +4 -0
- package/dist/app.js +11 -0
- package/dist/config.d.ts +18 -0
- package/dist/config.js +34 -0
- package/dist/internal/components/ask-assistant-bridge.d.ts +10 -0
- package/dist/internal/components/ask-assistant-bridge.js +50 -0
- package/dist/internal/components/dashboard-grid.d.ts +45 -0
- package/dist/internal/components/dashboard-grid.js +84 -0
- package/dist/internal/components/dev-toolbar.d.ts +15 -0
- package/dist/internal/components/dev-toolbar.js +155 -0
- package/dist/internal/components/harness-providers.d.ts +16 -0
- package/dist/internal/components/harness-providers.js +83 -0
- package/dist/internal/components/harness-widget-provider.d.ts +60 -0
- package/dist/internal/components/harness-widget-provider.js +84 -0
- package/dist/internal/components/i18n-provider.d.ts +9 -0
- package/dist/internal/components/i18n-provider.js +9 -0
- package/dist/internal/components/row-groups.d.ts +23 -0
- package/dist/internal/components/row-groups.js +37 -0
- package/dist/internal/components/surface-nav.d.ts +4 -0
- package/dist/internal/components/surface-nav.js +70 -0
- package/dist/internal/components/viewport-frame.d.ts +14 -0
- package/dist/internal/components/viewport-frame.js +27 -0
- package/dist/internal/components/widget-boundary.d.ts +25 -0
- package/dist/internal/components/widget-boundary.js +44 -0
- package/dist/internal/components/widget-surface.d.ts +20 -0
- package/dist/internal/components/widget-surface.js +76 -0
- package/dist/internal/lib/fonts.d.ts +2 -0
- package/dist/internal/lib/fonts.js +22 -0
- package/dist/internal/lib/harness-fetch-interceptor.d.ts +89 -0
- package/dist/internal/lib/harness-fetch-interceptor.js +101 -0
- package/dist/internal/lib/harness-live-fetch.d.ts +66 -0
- package/dist/internal/lib/harness-live-fetch.js +121 -0
- package/dist/internal/lib/harness-query-client.d.ts +43 -0
- package/dist/internal/lib/harness-query-client.js +103 -0
- package/dist/internal/lib/http-fixtures.d.ts +145 -0
- package/dist/internal/lib/http-fixtures.js +256 -0
- package/dist/internal/lib/i18n.d.ts +2 -0
- package/dist/internal/lib/i18n.js +17 -0
- package/dist/internal/lib/redact.d.ts +33 -0
- package/dist/internal/lib/redact.js +43 -0
- package/dist/internal/lib/toolbar-context.d.ts +59 -0
- package/dist/internal/lib/toolbar-context.js +124 -0
- package/dist/internal/registry-context.d.ts +27 -0
- package/dist/internal/registry-context.js +51 -0
- package/dist/internal/routes/activation-page.d.ts +33 -0
- package/dist/internal/routes/activation-page.js +242 -0
- package/dist/internal/routes/index-page.d.ts +13 -0
- package/dist/internal/routes/index-page.js +62 -0
- package/dist/internal/routes/integration-layout.d.ts +9 -0
- package/dist/internal/routes/integration-layout.js +84 -0
- package/dist/internal/routes/root-layout.d.ts +34 -0
- package/dist/internal/routes/root-layout.js +39 -0
- package/dist/internal/routes/single-widget-page.d.ts +6 -0
- package/dist/internal/routes/single-widget-page.js +30 -0
- package/dist/internal/routes/tile-page.d.ts +1 -0
- package/dist/internal/routes/tile-page.js +88 -0
- package/dist/internal/routes/triggers-page.d.ts +1 -0
- package/dist/internal/routes/triggers-page.js +386 -0
- package/dist/internal/routes/widgets-page.d.ts +1 -0
- package/dist/internal/routes/widgets-page.js +19 -0
- package/dist/internal/surfaces.d.ts +25 -0
- package/dist/internal/surfaces.js +29 -0
- package/dist/mocks/team-account-workspace.d.ts +78 -0
- package/dist/mocks/team-account-workspace.js +86 -0
- package/dist/registry.d.ts +418 -0
- package/dist/registry.js +82 -0
- package/dist/routes.d.ts +24 -0
- package/dist/routes.js +15 -0
- package/dist/styles.css +236 -0
- 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
|
+
};
|