@ekanos/sdk 0.3.0 → 0.5.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/README.md +171 -75
- package/api-report.md +8 -3
- package/dist/components/base-activation-dialog.d.ts +15 -5
- package/dist/components/base-activation-dialog.js +31 -5
- package/dist/components/base-activation-dialog.js.map +1 -1
- package/dist/components/base-activation-form.d.ts +20 -5
- package/dist/components/base-activation-form.js +12 -5
- package/dist/components/base-activation-form.js.map +1 -1
- package/dist/components/base-marketplace-tile.js +12 -10
- package/dist/components/base-marketplace-tile.js.map +1 -1
- package/dist/components/index.d.ts +1 -1
- package/dist/components/index.js.map +1 -1
- package/dist/components/oauth-activation-form.d.ts +23 -16
- package/dist/components/oauth-activation-form.js +34 -20
- package/dist/components/oauth-activation-form.js.map +1 -1
- package/dist/components/widgets/ai-prompt-chip.js +2 -2
- package/dist/components/widgets/ai-prompt-chip.js.map +1 -1
- package/dist/components/widgets/use-relative-time.d.ts +7 -0
- package/dist/components/widgets/use-relative-time.js +55 -0
- package/dist/components/widgets/use-relative-time.js.map +1 -0
- package/dist/components/widgets/widget-ask-bar.js +1 -1
- package/dist/components/widgets/widget-ask-bar.js.map +1 -1
- package/dist/components/widgets/widget-context.d.ts +21 -0
- package/dist/components/widgets/widget-context.js.map +1 -1
- package/dist/components/widgets/widget-preview-provider.d.ts +1 -1
- package/dist/components/widgets/widget-preview-provider.js +1 -1
- package/dist/components/widgets/widget-preview-provider.js.map +1 -1
- package/dist/components/widgets/widget-state-components.d.ts +4 -1
- package/dist/components/widgets/widget-state-components.js +6 -4
- package/dist/components/widgets/widget-state-components.js.map +1 -1
- package/dist/components/widgets/widget.d.ts +47 -2
- package/dist/components/widgets/widget.js +111 -23
- package/dist/components/widgets/widget.js.map +1 -1
- package/dist/context/egress.js.map +1 -1
- package/dist/context/storage-validation.d.ts +24 -4
- package/dist/context/storage-validation.js +5 -2
- package/dist/context/storage-validation.js.map +1 -1
- package/dist/hooks/ask-assistant-context.js.map +1 -1
- package/dist/hooks/fetch-integration-storage.js.map +1 -1
- package/dist/hooks/partner-oauth.js +3 -0
- package/dist/hooks/partner-oauth.js.map +1 -1
- package/dist/hooks/use-integration-action.js.map +1 -1
- package/dist/hooks/use-oauth-connection-status.d.ts +78 -78
- package/dist/hooks/use-oauth-connection-status.js +17 -10
- package/dist/hooks/use-oauth-connection-status.js.map +1 -1
- package/dist/hooks/widget-event-context.js.map +1 -1
- package/dist/integration/event-surfaces.js.map +1 -1
- package/dist/integration/index.d.ts +2 -2
- package/dist/integration/index.js +1 -1
- package/dist/integration/index.js.map +1 -1
- package/dist/testing/invoke.js +9 -5
- package/dist/testing/invoke.js.map +1 -1
- package/dist/testing/mock-context.js +43 -8
- package/dist/testing/mock-context.js.map +1 -1
- package/dist/testing/mock-fetch-provider.js.map +1 -1
- package/dist/types/integration.d.ts +27 -21
- package/dist/types/integration.js.map +1 -1
- package/dist/types/widget-ask-context.d.ts +9 -0
- package/dist/types/widget-ask-context.js.map +1 -1
- package/dist/types/workspace-target.d.ts +3 -2
- package/dist/types/workspace-target.js.map +1 -1
- package/eslint.preset.mjs +9 -8
- package/package.json +37 -41
package/dist/testing/invoke.js
CHANGED
|
@@ -28,11 +28,15 @@ export async function invokeWebhook(definition, webhookId, payload, options = {}
|
|
|
28
28
|
throw new Error(`Integration "${definition.slug}" declares no webhook "${webhookId}". ` +
|
|
29
29
|
`Declared webhook ids: ${listIds(((_b = definition.webhooks) !== null && _b !== void 0 ? _b : []).map((w) => w.id))}.`);
|
|
30
30
|
}
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
31
|
+
// The host's webhook route validates through Standard Schema too, so a
|
|
32
|
+
// partner test exercises the exact call production makes.
|
|
33
|
+
const parsed = await webhook.payloadSchema['~standard'].validate(payload);
|
|
34
|
+
if (parsed.issues) {
|
|
35
|
+
const issues = parsed.issues
|
|
34
36
|
.map((issue) => {
|
|
35
|
-
|
|
37
|
+
var _a;
|
|
38
|
+
const segments = ((_a = issue.path) !== null && _a !== void 0 ? _a : []).map((segment) => typeof segment === 'object' ? String(segment.key) : String(segment));
|
|
39
|
+
const path = segments.length > 0 ? segments.join('.') : '(root)';
|
|
36
40
|
return ` - ${path}: ${issue.message}`;
|
|
37
41
|
})
|
|
38
42
|
.join('\n');
|
|
@@ -53,7 +57,7 @@ export async function invokeWebhook(definition, webhookId, payload, options = {}
|
|
|
53
57
|
id: (_c = options.eventId) !== null && _c !== void 0 ? _c : `evt_${crypto.randomUUID()}`,
|
|
54
58
|
receivedAt: (_d = options.receivedAt) !== null && _d !== void 0 ? _d : new Date().toISOString(),
|
|
55
59
|
headers: (_e = options.headers) !== null && _e !== void 0 ? _e : {},
|
|
56
|
-
payload: parsed.
|
|
60
|
+
payload: parsed.value,
|
|
57
61
|
};
|
|
58
62
|
const result = await webhook.handler(ctx, event);
|
|
59
63
|
return { result, event, ctx };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"invoke.js","sourceRoot":"","sources":["../../src/testing/invoke.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Local execution of the declared event surfaces — `invokeWebhook()` and\n * `invokeSchedule()` deliver to a definition's handlers exactly the way a\n * transport would, against a `createMockContext()` built FROM the definition\n * (its slug, storage schemas, and egress list), so a handler unit test\n * exercises the same schemas and the same egress allowlist production will.\n * `invokeActivate()` does the same for the `onActivate` lifecycle hook.\n *\n * Transport semantics reproduced here, so they cannot drift from the docs:\n * - the payload is parsed against `payloadSchema` BEFORE the handler runs;\n * an invalid payload throws and the handler never sees it;\n * - signature verification is the TRANSPORT's job — the local transport\n * logs it as skipped (one warn line on `ctx.logs`), it never verifies and\n * the handler never does either;\n * - a schedule invocation carries `{scheduledFor, invokedAt, trigger}` with\n * `trigger: 'manual'` by default (a human pressed the button);\n * - `invokeActivate()` simply awaits `onActivate(ctx)` and returns the\n * context it ran against — a throw PROPAGATES here. The non-fatal\n * (log-and-continue) handling described on `OnActivateHandler` is HOST\n * POLICY, applied by the transport that calls the hook in production\n * (and by the harness's activation surface locally); this helper is a\n * bare invoker; a test asserting the non-fatal behavior should catch its\n * own rejection.\n */\nimport type {\n IntegrationDefinition,\n ScheduleInvocation,\n ScheduleResult,\n StorageSchemas,\n WebhookEvent,\n WebhookResult,\n} from '@ekanos/integration-schema';\n\nimport {\n type MockContextOptions,\n type MockIntegrationContext,\n createMockContext,\n} from './mock-context';\n\n/**\n * Everything `createMockContext` takes except the fields the definition\n * itself is the authority on — the helpers derive `integration`,\n * `storageSchemas`, and `egress` from the definition so a test cannot\n * accidentally run a handler against schemas or an allowlist the definition\n * does not declare.\n */\nexport type DefinitionContextOptions<\n Schemas extends StorageSchemas = StorageSchemas,\n> = Omit<\n MockContextOptions<Schemas>,\n 'integration' | 'storageSchemas' | 'egress'\n>;\n\ninterface BaseInvokeOptions<Schemas extends StorageSchemas> {\n /**\n * Reuse an existing mock context so state (storage, secrets, logs)\n * accumulates across invocations — the harness does this. When set,\n * `context` wins and `contextOptions` must be omitted.\n */\n context?: MockIntegrationContext<Schemas>;\n /** Seeds and stubs for the context the helper creates. */\n contextOptions?: DefinitionContextOptions<Schemas>;\n}\n\nexport interface InvokeWebhookOptions<\n Schemas extends StorageSchemas = StorageSchemas,\n> extends BaseInvokeOptions<Schemas> {\n /** Delivery headers the event carries. Defaults to `{}`. */\n headers?: Record<string, string>;\n /** Transport-assigned event id. Defaults to a random UUID. */\n eventId?: string;\n /** ISO receipt time. Defaults to now. */\n receivedAt?: string;\n}\n\nexport interface InvokeScheduleOptions<\n Schemas extends StorageSchemas = StorageSchemas,\n> extends BaseInvokeOptions<Schemas> {\n /** Defaults to `'manual'` — a human pressed \"Run now\". */\n trigger?: ScheduleInvocation['trigger'];\n /** The tick this invocation stands for. Defaults to now. */\n scheduledFor?: string;\n}\n\nexport type InvokeActivateOptions<\n Schemas extends StorageSchemas = StorageSchemas,\n> = BaseInvokeOptions<Schemas>;\n\nexport interface ActivateInvocationOutcome<\n Schemas extends StorageSchemas = StorageSchemas,\n> {\n /** The context the handler ran against — assert on its recordings. */\n ctx: MockIntegrationContext<Schemas>;\n}\n\nexport interface WebhookInvocationOutcome<\n Schemas extends StorageSchemas = StorageSchemas,\n> {\n result: WebhookResult;\n /** The event the handler received (payload already schema-parsed). */\n event: WebhookEvent;\n /** The context the handler ran against — assert on its recordings. */\n ctx: MockIntegrationContext<Schemas>;\n}\n\nexport interface ScheduleInvocationOutcome<\n Schemas extends StorageSchemas = StorageSchemas,\n> {\n result: ScheduleResult;\n invocation: ScheduleInvocation;\n ctx: MockIntegrationContext<Schemas>;\n}\n\nfunction contextFor<Schemas extends StorageSchemas>(\n definition: IntegrationDefinition<Schemas>,\n options: BaseInvokeOptions<Schemas>,\n): MockIntegrationContext<Schemas> {\n if (options.context) {\n if (options.contextOptions) {\n throw new TypeError(\n 'Pass either `context` (reuse an existing mock context) or ' +\n '`contextOptions` (seed a fresh one), not both — seeds cannot be ' +\n 'applied to a context that already exists.',\n );\n }\n return options.context;\n }\n\n return createMockContext<Schemas>({\n ...(options.contextOptions ?? {}),\n integration: { slug: definition.slug },\n ...(definition.storage ? { storageSchemas: definition.storage } : {}),\n egress: definition.egress ?? [],\n });\n}\n\nfunction listIds(ids: readonly string[]): string {\n return ids.length > 0 ? ids.map((id) => `\"${id}\"`).join(', ') : '(none)';\n}\n\n/**\n * Delivers one payload to one declared webhook, the way a transport would.\n * Throws if the id is undeclared or the payload fails `payloadSchema`;\n * returns the handler's result plus the event and the context it ran\n * against. Never verifies signatures — that is the transport's job, and the\n * local transport records the skip as a `warn` log line.\n */\nexport async function invokeWebhook<\n Schemas extends StorageSchemas = StorageSchemas,\n>(\n definition: IntegrationDefinition<Schemas>,\n webhookId: string,\n payload: unknown,\n options: InvokeWebhookOptions<Schemas> = {},\n): Promise<WebhookInvocationOutcome<Schemas>> {\n const webhook = (definition.webhooks ?? []).find((w) => w.id === webhookId);\n\n if (!webhook) {\n throw new Error(\n `Integration \"${definition.slug}\" declares no webhook \"${webhookId}\". ` +\n `Declared webhook ids: ${listIds((definition.webhooks ?? []).map((w) => w.id))}.`,\n );\n }\n\n const parsed = webhook.payloadSchema.safeParse(payload);\n\n if (!parsed.success) {\n const issues = parsed.error.issues\n .map((issue) => {\n const path = issue.path.length > 0 ? issue.path.join('.') : '(root)';\n return ` - ${path}: ${issue.message}`;\n })\n .join('\\n');\n throw new Error(\n `Payload rejected by webhook \"${webhookId}\"'s payloadSchema — the ` +\n `transport refuses such a delivery before the handler runs:\\n${issues}`,\n );\n }\n\n const ctx = contextFor(definition, options);\n\n if (webhook.signature !== 'none') {\n ctx.logger.warn(\n {\n webhookId,\n header: webhook.signature.header,\n secretName: webhook.signature.secretName,\n },\n 'Signature verification SKIPPED (local transport). The host ingress ' +\n 'verifies this header against the named secret before the handler ' +\n 'runs — handlers never verify signatures themselves.',\n );\n }\n\n const event: WebhookEvent = {\n id: options.eventId ?? `evt_${crypto.randomUUID()}`,\n receivedAt: options.receivedAt ?? new Date().toISOString(),\n headers: options.headers ?? {},\n payload: parsed.data,\n };\n\n const result = await webhook.handler(ctx, event);\n\n return { result, event, ctx };\n}\n\n/**\n * Fires one declared schedule, the way the scheduler would. Throws if the id\n * is undeclared; returns the handler's result plus the invocation and the\n * context it ran against.\n */\nexport async function invokeSchedule<\n Schemas extends StorageSchemas = StorageSchemas,\n>(\n definition: IntegrationDefinition<Schemas>,\n scheduleId: string,\n options: InvokeScheduleOptions<Schemas> = {},\n): Promise<ScheduleInvocationOutcome<Schemas>> {\n const schedule = (definition.schedules ?? []).find(\n (s) => s.id === scheduleId,\n );\n\n if (!schedule) {\n throw new Error(\n `Integration \"${definition.slug}\" declares no schedule \"${scheduleId}\". ` +\n `Declared schedule ids: ${listIds((definition.schedules ?? []).map((s) => s.id))}.`,\n );\n }\n\n const ctx = contextFor(definition, options);\n const now = new Date().toISOString();\n\n const invocation: ScheduleInvocation = {\n scheduledFor: options.scheduledFor ?? now,\n invokedAt: now,\n trigger: options.trigger ?? 'manual',\n };\n\n const result = await schedule.handler(ctx, invocation);\n\n return { result, invocation, ctx };\n}\n\n/**\n * Runs the definition's declared `onActivate` hook, the way the host would\n * after an activation persists or activationData updates. Throws if the\n * definition declares no `onActivate` — there is nothing to invoke, and a\n * silent no-op would let a test believe it exercised a hook that does not\n * exist. A throwing handler PROPAGATES from this helper: the non-fatal\n * (log-a-warning, keep the activation) handling is host policy applied by\n * whatever calls this in production/the harness, not by this bare invoker\n * (see the module doc comment).\n */\nexport async function invokeActivate<\n Schemas extends StorageSchemas = StorageSchemas,\n>(\n definition: IntegrationDefinition<Schemas>,\n options: InvokeActivateOptions<Schemas> = {},\n): Promise<ActivateInvocationOutcome<Schemas>> {\n if (!definition.onActivate) {\n throw new Error(\n `Integration \"${definition.slug}\" declares no onActivate hook — add ` +\n `one to defineIntegration() before invoking it.`,\n );\n }\n\n const ctx = contextFor(definition, options);\n\n await definition.onActivate(ctx);\n\n return { ctx };\n}\n"]}
|
|
1
|
+
{"version":3,"file":"invoke.js","sourceRoot":"","sources":["../../src/testing/invoke.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Local execution of the declared event surfaces — `invokeWebhook()` and\n * `invokeSchedule()` deliver to a definition's handlers exactly the way a\n * transport would, against a `createMockContext()` built FROM the definition\n * (its slug, storage schemas, and egress list), so a handler unit test\n * exercises the same schemas and the same egress allowlist production will.\n * `invokeActivate()` does the same for the `onActivate` lifecycle hook.\n *\n * Transport semantics reproduced here, so they cannot drift from the docs:\n * - the payload is parsed against `payloadSchema` BEFORE the handler runs;\n * an invalid payload throws and the handler never sees it;\n * - signature verification is the TRANSPORT's job — the local transport\n * logs it as skipped (one warn line on `ctx.logs`), it never verifies and\n * the handler never does either;\n * - a schedule invocation carries `{scheduledFor, invokedAt, trigger}` with\n * `trigger: 'manual'` by default (a human pressed the button);\n * - `invokeActivate()` simply awaits `onActivate(ctx)` and returns the\n * context it ran against — a throw PROPAGATES here. The non-fatal\n * (log-and-continue) handling described on `OnActivateHandler` is HOST\n * POLICY, applied by the transport that calls the hook in production\n * (and by the harness's activation surface locally); this helper is a\n * bare invoker; a test asserting the non-fatal behavior should catch its\n * own rejection.\n */\nimport type {\n IntegrationDefinition,\n ScheduleInvocation,\n ScheduleResult,\n StorageSchemas,\n WebhookEvent,\n WebhookResult,\n} from '@ekanos/integration-schema';\n\nimport {\n type MockContextOptions,\n type MockIntegrationContext,\n createMockContext,\n} from './mock-context';\n\n/**\n * Everything `createMockContext` takes except the fields the definition\n * itself is the authority on — the helpers derive `integration`,\n * `storageSchemas`, and `egress` from the definition so a test cannot\n * accidentally run a handler against schemas or an allowlist the definition\n * does not declare.\n */\nexport type DefinitionContextOptions<\n Schemas extends StorageSchemas = StorageSchemas,\n> = Omit<\n MockContextOptions<Schemas>,\n 'integration' | 'storageSchemas' | 'egress'\n>;\n\ninterface BaseInvokeOptions<Schemas extends StorageSchemas> {\n /**\n * Reuse an existing mock context so state (storage, secrets, logs)\n * accumulates across invocations — the harness does this. When set,\n * `context` wins and `contextOptions` must be omitted.\n */\n context?: MockIntegrationContext<Schemas>;\n /** Seeds and stubs for the context the helper creates. */\n contextOptions?: DefinitionContextOptions<Schemas>;\n}\n\nexport interface InvokeWebhookOptions<\n Schemas extends StorageSchemas = StorageSchemas,\n> extends BaseInvokeOptions<Schemas> {\n /** Delivery headers the event carries. Defaults to `{}`. */\n headers?: Record<string, string>;\n /** Transport-assigned event id. Defaults to a random UUID. */\n eventId?: string;\n /** ISO receipt time. Defaults to now. */\n receivedAt?: string;\n}\n\nexport interface InvokeScheduleOptions<\n Schemas extends StorageSchemas = StorageSchemas,\n> extends BaseInvokeOptions<Schemas> {\n /** Defaults to `'manual'` — a human pressed \"Run now\". */\n trigger?: ScheduleInvocation['trigger'];\n /** The tick this invocation stands for. Defaults to now. */\n scheduledFor?: string;\n}\n\nexport type InvokeActivateOptions<\n Schemas extends StorageSchemas = StorageSchemas,\n> = BaseInvokeOptions<Schemas>;\n\nexport interface ActivateInvocationOutcome<\n Schemas extends StorageSchemas = StorageSchemas,\n> {\n /** The context the handler ran against — assert on its recordings. */\n ctx: MockIntegrationContext<Schemas>;\n}\n\nexport interface WebhookInvocationOutcome<\n Schemas extends StorageSchemas = StorageSchemas,\n> {\n result: WebhookResult;\n /** The event the handler received (payload already schema-parsed). */\n event: WebhookEvent;\n /** The context the handler ran against — assert on its recordings. */\n ctx: MockIntegrationContext<Schemas>;\n}\n\nexport interface ScheduleInvocationOutcome<\n Schemas extends StorageSchemas = StorageSchemas,\n> {\n result: ScheduleResult;\n invocation: ScheduleInvocation;\n ctx: MockIntegrationContext<Schemas>;\n}\n\nfunction contextFor<Schemas extends StorageSchemas>(\n definition: IntegrationDefinition<Schemas>,\n options: BaseInvokeOptions<Schemas>,\n): MockIntegrationContext<Schemas> {\n if (options.context) {\n if (options.contextOptions) {\n throw new TypeError(\n 'Pass either `context` (reuse an existing mock context) or ' +\n '`contextOptions` (seed a fresh one), not both — seeds cannot be ' +\n 'applied to a context that already exists.',\n );\n }\n return options.context;\n }\n\n return createMockContext<Schemas>({\n ...(options.contextOptions ?? {}),\n integration: { slug: definition.slug },\n ...(definition.storage ? { storageSchemas: definition.storage } : {}),\n egress: definition.egress ?? [],\n });\n}\n\nfunction listIds(ids: readonly string[]): string {\n return ids.length > 0 ? ids.map((id) => `\"${id}\"`).join(', ') : '(none)';\n}\n\n/**\n * Delivers one payload to one declared webhook, the way a transport would.\n * Throws if the id is undeclared or the payload fails `payloadSchema`;\n * returns the handler's result plus the event and the context it ran\n * against. Never verifies signatures — that is the transport's job, and the\n * local transport records the skip as a `warn` log line.\n */\nexport async function invokeWebhook<\n Schemas extends StorageSchemas = StorageSchemas,\n>(\n definition: IntegrationDefinition<Schemas>,\n webhookId: string,\n payload: unknown,\n options: InvokeWebhookOptions<Schemas> = {},\n): Promise<WebhookInvocationOutcome<Schemas>> {\n const webhook = (definition.webhooks ?? []).find((w) => w.id === webhookId);\n\n if (!webhook) {\n throw new Error(\n `Integration \"${definition.slug}\" declares no webhook \"${webhookId}\". ` +\n `Declared webhook ids: ${listIds((definition.webhooks ?? []).map((w) => w.id))}.`,\n );\n }\n\n // The host's webhook route validates through Standard Schema too, so a\n // partner test exercises the exact call production makes.\n const parsed = await webhook.payloadSchema['~standard'].validate(payload);\n\n if (parsed.issues) {\n const issues = parsed.issues\n .map((issue) => {\n const segments = (issue.path ?? []).map((segment) =>\n typeof segment === 'object' ? String(segment.key) : String(segment),\n );\n const path = segments.length > 0 ? segments.join('.') : '(root)';\n return ` - ${path}: ${issue.message}`;\n })\n .join('\\n');\n throw new Error(\n `Payload rejected by webhook \"${webhookId}\"'s payloadSchema — the ` +\n `transport refuses such a delivery before the handler runs:\\n${issues}`,\n );\n }\n\n const ctx = contextFor(definition, options);\n\n if (webhook.signature !== 'none') {\n ctx.logger.warn(\n {\n webhookId,\n header: webhook.signature.header,\n secretName: webhook.signature.secretName,\n },\n 'Signature verification SKIPPED (local transport). The host ingress ' +\n 'verifies this header against the named secret before the handler ' +\n 'runs — handlers never verify signatures themselves.',\n );\n }\n\n const event: WebhookEvent = {\n id: options.eventId ?? `evt_${crypto.randomUUID()}`,\n receivedAt: options.receivedAt ?? new Date().toISOString(),\n headers: options.headers ?? {},\n payload: parsed.value,\n };\n\n const result = await webhook.handler(ctx, event);\n\n return { result, event, ctx };\n}\n\n/**\n * Fires one declared schedule, the way the scheduler would. Throws if the id\n * is undeclared; returns the handler's result plus the invocation and the\n * context it ran against.\n */\nexport async function invokeSchedule<\n Schemas extends StorageSchemas = StorageSchemas,\n>(\n definition: IntegrationDefinition<Schemas>,\n scheduleId: string,\n options: InvokeScheduleOptions<Schemas> = {},\n): Promise<ScheduleInvocationOutcome<Schemas>> {\n const schedule = (definition.schedules ?? []).find(\n (s) => s.id === scheduleId,\n );\n\n if (!schedule) {\n throw new Error(\n `Integration \"${definition.slug}\" declares no schedule \"${scheduleId}\". ` +\n `Declared schedule ids: ${listIds((definition.schedules ?? []).map((s) => s.id))}.`,\n );\n }\n\n const ctx = contextFor(definition, options);\n const now = new Date().toISOString();\n\n const invocation: ScheduleInvocation = {\n scheduledFor: options.scheduledFor ?? now,\n invokedAt: now,\n trigger: options.trigger ?? 'manual',\n };\n\n const result = await schedule.handler(ctx, invocation);\n\n return { result, invocation, ctx };\n}\n\n/**\n * Runs the definition's declared `onActivate` hook, the way the host would\n * after an activation persists or activationData updates. Throws if the\n * definition declares no `onActivate` — there is nothing to invoke, and a\n * silent no-op would let a test believe it exercised a hook that does not\n * exist. A throwing handler PROPAGATES from this helper: the non-fatal\n * (log-a-warning, keep the activation) handling is host policy applied by\n * whatever calls this in production/the harness, not by this bare invoker\n * (see the module doc comment).\n */\nexport async function invokeActivate<\n Schemas extends StorageSchemas = StorageSchemas,\n>(\n definition: IntegrationDefinition<Schemas>,\n options: InvokeActivateOptions<Schemas> = {},\n): Promise<ActivateInvocationOutcome<Schemas>> {\n if (!definition.onActivate) {\n throw new Error(\n `Integration \"${definition.slug}\" declares no onActivate hook — add ` +\n `one to defineIntegration() before invoking it.`,\n );\n }\n\n const ctx = contextFor(definition, options);\n\n await definition.onActivate(ctx);\n\n return { ctx };\n}\n"]}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
import { PARTNER_STORAGE_LIMITS } from '@ekanos/integration-schema';
|
|
1
2
|
import { isEgressAllowed } from '../context/egress.js';
|
|
2
|
-
import { EgressDeniedError, SecretAccessError } from '../context/errors.js';
|
|
3
|
+
import { EgressDeniedError, SecretAccessError, StorageValidationError, } from '../context/errors.js';
|
|
3
4
|
import { assertStorageKeyDeclared, assertStorageKeyWritable, parseStorageValue, } from '../context/storage-validation.js';
|
|
4
5
|
import { matchFetchHandler } from './fetch-matching.js';
|
|
5
6
|
function isExpired(row) {
|
|
@@ -53,6 +54,20 @@ function createInMemoryStore(args) {
|
|
|
53
54
|
schemas,
|
|
54
55
|
value: data,
|
|
55
56
|
});
|
|
57
|
+
// The host's per-value cap (finding E), enforced here with the host's
|
|
58
|
+
// own wording so `ekanos test` fails where production would.
|
|
59
|
+
const serializedBytes = utf8Bytes(JSON.stringify(parsed));
|
|
60
|
+
if (serializedBytes > PARTNER_STORAGE_LIMITS.maxSerializedValueBytes) {
|
|
61
|
+
throw new StorageValidationError({
|
|
62
|
+
scope,
|
|
63
|
+
key,
|
|
64
|
+
hint: `The value written to ${scope} storage key "${key}" serializes ` +
|
|
65
|
+
`to ${serializedBytes} bytes, above the ` +
|
|
66
|
+
`${PARTNER_STORAGE_LIMITS.maxSerializedValueBytes}-byte per-value ` +
|
|
67
|
+
`limit. Store less per key: trim cached payloads, or split state ` +
|
|
68
|
+
`across purpose-named keys.`,
|
|
69
|
+
});
|
|
70
|
+
}
|
|
56
71
|
rows.set(key, {
|
|
57
72
|
data: parsed,
|
|
58
73
|
externalId: (_a = options === null || options === void 0 ? void 0 : options.externalId) !== null && _a !== void 0 ? _a : null,
|
|
@@ -68,6 +83,10 @@ function createInMemoryStore(args) {
|
|
|
68
83
|
},
|
|
69
84
|
};
|
|
70
85
|
}
|
|
86
|
+
/** Byte length of a string as UTF-8, without Buffer (this module runs in the browser-side harness too). */
|
|
87
|
+
function utf8Bytes(value) {
|
|
88
|
+
return new TextEncoder().encode(value).length;
|
|
89
|
+
}
|
|
71
90
|
function dumpScope(rows) {
|
|
72
91
|
const out = {};
|
|
73
92
|
for (const [key, row] of rows) {
|
|
@@ -77,12 +96,13 @@ function dumpScope(rows) {
|
|
|
77
96
|
return out;
|
|
78
97
|
}
|
|
79
98
|
export function createMockContext(options) {
|
|
80
|
-
var _a, _b, _c, _d
|
|
99
|
+
var _a, _b, _c, _d;
|
|
100
|
+
var _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q;
|
|
81
101
|
const userId = options.userId !== undefined ? options.userId : 'mock-user-id';
|
|
82
|
-
const actor = (
|
|
102
|
+
const actor = (_e = options.actor) !== null && _e !== void 0 ? _e : (userId !== null
|
|
83
103
|
? { kind: 'user', userId }
|
|
84
104
|
: { kind: 'machine', tokenId: 'mock-token-id', createdBy: null });
|
|
85
|
-
const schemas = (
|
|
105
|
+
const schemas = (_f = options.storageSchemas) !== null && _f !== void 0 ? _f : {};
|
|
86
106
|
const accountRows = new Map();
|
|
87
107
|
const userRows = new Map();
|
|
88
108
|
const seedScope = (scope, rows, seed) => {
|
|
@@ -102,8 +122,8 @@ export function createMockContext(options) {
|
|
|
102
122
|
});
|
|
103
123
|
}
|
|
104
124
|
};
|
|
105
|
-
seedScope('account', accountRows, (
|
|
106
|
-
seedScope('user', userRows, (
|
|
125
|
+
seedScope('account', accountRows, (_a = options.storage) === null || _a === void 0 ? void 0 : _a.account);
|
|
126
|
+
seedScope('user', userRows, (_b = options.storage) === null || _b === void 0 ? void 0 : _b.user);
|
|
107
127
|
const guardUserScope = () => {
|
|
108
128
|
if (actor.kind === 'machine') {
|
|
109
129
|
throw new Error('ctx.storage.user is unavailable for machine actors: there is no ' +
|
|
@@ -128,8 +148,8 @@ export function createMockContext(options) {
|
|
|
128
148
|
guard: guardUserScope,
|
|
129
149
|
}),
|
|
130
150
|
};
|
|
131
|
-
const accountSecrets = new Map(Object.entries((
|
|
132
|
-
const adminSecrets = new Map(Object.entries((_h = (
|
|
151
|
+
const accountSecrets = new Map(Object.entries((_g = (_c = options.secrets) === null || _c === void 0 ? void 0 : _c.account) !== null && _g !== void 0 ? _g : {}));
|
|
152
|
+
const adminSecrets = new Map(Object.entries((_h = (_d = options.secrets) === null || _d === void 0 ? void 0 : _d.admin) !== null && _h !== void 0 ? _h : {}));
|
|
133
153
|
const secrets = {
|
|
134
154
|
async get(name) {
|
|
135
155
|
var _a, _b;
|
|
@@ -144,6 +164,21 @@ export function createMockContext(options) {
|
|
|
144
164
|
if (!accountSecrets.has(name) && adminSecrets.has(name)) {
|
|
145
165
|
throw new SecretAccessError(name);
|
|
146
166
|
}
|
|
167
|
+
// The host's caps (finding E): one Vault record per value, one row +
|
|
168
|
+
// record per distinct name. Same wording as the host.
|
|
169
|
+
const valueBytes = utf8Bytes(value);
|
|
170
|
+
if (valueBytes > PARTNER_STORAGE_LIMITS.maxSerializedValueBytes) {
|
|
171
|
+
throw new TypeError(`secrets.set("${name}", …): the value is ${valueBytes} bytes, ` +
|
|
172
|
+
`above the ${PARTNER_STORAGE_LIMITS.maxSerializedValueBytes}-byte ` +
|
|
173
|
+
`per-secret limit. Secrets hold credentials, not payloads — store ` +
|
|
174
|
+
`bulk data under a ctx.storage key instead.`);
|
|
175
|
+
}
|
|
176
|
+
if (!accountSecrets.has(name) &&
|
|
177
|
+
accountSecrets.size >= PARTNER_STORAGE_LIMITS.maxSecretNames) {
|
|
178
|
+
throw new Error(`secrets.set("${name}", …): this integration already stores ` +
|
|
179
|
+
`${PARTNER_STORAGE_LIMITS.maxSecretNames} distinct secret names, ` +
|
|
180
|
+
`the per-integration limit. Reuse or rotate an existing name.`);
|
|
181
|
+
}
|
|
147
182
|
accountSecrets.set(name, value);
|
|
148
183
|
},
|
|
149
184
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mock-context.js","sourceRoot":"","sources":["../../src/testing/mock-context.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * `createMockContext()` — the day-1 in-memory `IntegrationContext` for the\n * partner harness (proposal §4, amended by rulings 1–4). Self-contained:\n * nothing here touches Supabase, Vault, or the network. Enforcement is the\n * SAME shared implementation the production wrapper uses\n * (`@ekanos/sdk/context`): schema validation via `parseStorageValue`, egress\n * via `isEgressAllowed` — a test that passes the mock cannot be a schema or\n * egress surprise later.\n */\nimport type { z } from 'zod';\n\nimport { isEgressAllowed } from '../context/egress';\nimport { EgressDeniedError, SecretAccessError } from '../context/errors';\nimport {\n assertStorageKeyDeclared,\n assertStorageKeyWritable,\n parseStorageValue,\n} from '../context/storage-validation';\nimport type {\n IntegrationActor,\n IntegrationContext,\n IntegrationFetch,\n IntegrationLogger,\n IntegrationSecrets,\n IntegrationStorage,\n ScopedStore,\n StorageEntry,\n StorageKeySchema,\n StorageSchemaMap,\n StorageSchemas,\n StorageWriteOptions,\n} from '../context/types';\nimport { type MockFetchHandler, matchFetchHandler } from './fetch-matching';\n\ntype StorageScope = 'account' | 'user';\n\n// Seeds are typed off the DECLARED schema, resolved out of whichever\n// declaration form the key used — a bare zod schema or a\n// `{ schema, clientReadable }` descriptor.\ntype ScopeSeed<M> = M extends StorageSchemaMap\n ? { [K in keyof M & string]?: z.input<StorageKeySchema<M[K]>> }\n : Record<never, never>;\n\n// Re-exported from here for backward compatibility — `MockFetchHandler`'s\n// canonical home is now `./fetch-matching`, shared with `createMockFetchProvider`.\nexport type { MockFetchHandler } from './fetch-matching';\n\nexport interface MockContextOptions<\n Schemas extends StorageSchemas = StorageSchemas,\n> {\n accountId?: string;\n accountSlug?: string | null;\n userId?: string | null;\n actor?: IntegrationActor;\n sourceId?: string | null;\n timezone?: string | null;\n integration: { slug: string; productId?: string };\n /**\n * The storage declaration `defineIntegration({ storage })` will carry —\n * the mock enforces the identical schemas via the same shared validator\n * (ruling 1). Omit it and every storage access throws.\n */\n storageSchemas?: Schemas;\n /** Seed rows: key → data, per scope. Validated on construction via the write path. */\n storage?: {\n account?: ScopeSeed<Schemas['account']>;\n user?: ScopeSeed<Schemas['user']>;\n };\n /**\n * Seed secrets, by tier (ruling 2): `account` values are the rows this\n * integration stored — writable via `secrets.set()`; `admin` values stand\n * in for admin-issued source/global-tier credentials — readable, and a\n * `set()` against a name resolvable only there throws `SecretAccessError`.\n */\n secrets?: {\n account?: Record<string, string>;\n admin?: Record<string, string>;\n };\n /** Same allowlist the manifest would declare — enforced by the stub. */\n egress?: readonly string[];\n /** Programmable responses; unmatched allowed calls resolve 200 `{}`. */\n fetchHandlers?: MockFetchHandler[];\n}\n\nexport interface RecordedFetchCall {\n url: string;\n init?: RequestInit;\n denied: boolean;\n}\n\nexport interface RecordedLog {\n level: 'debug' | 'info' | 'warn' | 'error';\n context: Record<string, unknown>;\n message: string;\n}\n\nexport interface MockIntegrationContext<\n Schemas extends StorageSchemas = StorageSchemas,\n> extends IntegrationContext<Schemas> {\n /** Every ctx.fetch call, allowed or denied, in order. */\n readonly fetchCalls: readonly RecordedFetchCall[];\n /** Every log line, per level. */\n readonly logs: readonly RecordedLog[];\n /** Current readable storage state (expired entries excluded), key → data. */\n dumpStorage(): {\n account: Record<string, unknown>;\n user: Record<string, unknown>;\n };\n}\n\ninterface StoredRow {\n data: unknown;\n externalId: string | null;\n expiresAt: string | null;\n updatedAt: string;\n}\n\nfunction isExpired(row: StoredRow): boolean {\n return row.expiresAt !== null && Date.parse(row.expiresAt) <= Date.now();\n}\n\nfunction parseExpiresAt(\n options: StorageWriteOptions | undefined,\n): string | null {\n const expiresAt = options?.expiresAt;\n if (expiresAt === undefined) return null;\n if (Number.isNaN(Date.parse(expiresAt))) {\n throw new TypeError(\n `expiresAt \"${expiresAt}\" is not a parseable timestamp. Pass an ` +\n `ISO-8601 string, e.g. new Date(Date.now() + 60_000).toISOString().`,\n );\n }\n return expiresAt;\n}\n\nfunction createInMemoryStore(args: {\n scope: StorageScope;\n schemas: StorageSchemaMap | undefined;\n rows: Map<string, StoredRow>;\n guard: () => void;\n}): ScopedStore {\n const { scope, schemas, rows, guard } = args;\n\n return {\n async get(key: string): Promise<StorageEntry<unknown> | null> {\n guard();\n assertStorageKeyDeclared({ scope, key, schemas });\n const row = rows.get(key);\n if (!row) return null;\n if (isExpired(row)) {\n rows.delete(key);\n return null;\n }\n const data = parseStorageValue({\n scope,\n key,\n operation: 'read',\n schemas,\n value: row.data,\n });\n return {\n data,\n externalId: row.externalId,\n expiresAt: row.expiresAt,\n updatedAt: row.updatedAt,\n };\n },\n\n async set(\n key: string,\n data: unknown,\n options?: StorageWriteOptions,\n ): Promise<void> {\n guard();\n assertStorageKeyWritable({ scope, key });\n const parsed = parseStorageValue({\n scope,\n key,\n operation: 'write',\n schemas,\n value: data,\n });\n rows.set(key, {\n data: parsed,\n externalId: options?.externalId ?? null,\n expiresAt: parseExpiresAt(options),\n updatedAt: new Date().toISOString(),\n });\n },\n\n async delete(key: string): Promise<void> {\n guard();\n assertStorageKeyWritable({ scope, key });\n assertStorageKeyDeclared({ scope, key, schemas });\n rows.delete(key);\n },\n };\n}\n\nfunction dumpScope(rows: Map<string, StoredRow>): Record<string, unknown> {\n const out: Record<string, unknown> = {};\n for (const [key, row] of rows) {\n if (!isExpired(row)) out[key] = row.data;\n }\n return out;\n}\n\nexport function createMockContext<\n Schemas extends StorageSchemas = StorageSchemas,\n>(options: MockContextOptions<Schemas>): MockIntegrationContext<Schemas> {\n const userId = options.userId !== undefined ? options.userId : 'mock-user-id';\n const actor: IntegrationActor =\n options.actor ??\n (userId !== null\n ? { kind: 'user', userId }\n : { kind: 'machine', tokenId: 'mock-token-id', createdBy: null });\n\n const schemas: StorageSchemas = options.storageSchemas ?? {};\n\n const accountRows = new Map<string, StoredRow>();\n const userRows = new Map<string, StoredRow>();\n\n const seedScope = (\n scope: StorageScope,\n rows: Map<string, StoredRow>,\n seed: Record<string, unknown> | undefined,\n ) => {\n for (const [key, value] of Object.entries(seed ?? {})) {\n const parsed = parseStorageValue({\n scope,\n key,\n operation: 'write',\n schemas: schemas[scope],\n value,\n });\n rows.set(key, {\n data: parsed,\n externalId: null,\n expiresAt: null,\n updatedAt: new Date().toISOString(),\n });\n }\n };\n seedScope('account', accountRows, options.storage?.account);\n seedScope('user', userRows, options.storage?.user);\n\n const guardUserScope = () => {\n if (actor.kind === 'machine') {\n throw new Error(\n 'ctx.storage.user is unavailable for machine actors: there is no ' +\n 'user to scope the row to. Store machine-written state in ' +\n 'ctx.storage.account instead.',\n );\n }\n };\n\n // The stores validate every key and value at runtime via the shared\n // validator; the cast narrows the runtime-checked store to the\n // schema-typed interface the declaration promises.\n const storage = {\n account: createInMemoryStore({\n scope: 'account',\n schemas: schemas.account,\n rows: accountRows,\n guard: () => void 0,\n }),\n user: createInMemoryStore({\n scope: 'user',\n schemas: schemas.user,\n rows: userRows,\n guard: guardUserScope,\n }),\n } as unknown as IntegrationStorage<Schemas>;\n\n const accountSecrets = new Map(\n Object.entries(options.secrets?.account ?? {}),\n );\n const adminSecrets = new Map(Object.entries(options.secrets?.admin ?? {}));\n\n const secrets: IntegrationSecrets = {\n async get(name) {\n return accountSecrets.get(name) ?? adminSecrets.get(name) ?? null;\n },\n async names() {\n return [\n ...new Set([...accountSecrets.keys(), ...adminSecrets.keys()]),\n ].sort();\n },\n async set(name, value) {\n if (!accountSecrets.has(name) && adminSecrets.has(name)) {\n throw new SecretAccessError(name);\n }\n accountSecrets.set(name, value);\n },\n };\n\n const fetchCalls: RecordedFetchCall[] = [];\n const egress = options.egress ?? [];\n const fetchHandlers = options.fetchHandlers ?? [];\n\n // Not an async function: a denied call must throw synchronously, before\n // any I/O (proposal §2.3) — same behavior the production wrapper has.\n const fetch: IntegrationFetch = (input, init) => {\n const url = new URL(input).toString();\n const allowed = isEgressAllowed(url, egress);\n fetchCalls.push(\n init === undefined\n ? { url, denied: !allowed }\n : { url, init, denied: !allowed },\n );\n if (!allowed) {\n throw new EgressDeniedError(new URL(url).origin);\n }\n const handler = matchFetchHandler(url, fetchHandlers);\n if (handler) {\n return Promise.resolve(handler.respond(new Request(url, init)));\n }\n return Promise.resolve(\n new Response('{}', {\n status: 200,\n headers: { 'content-type': 'application/json' },\n }),\n );\n };\n\n const logs: RecordedLog[] = [];\n const record =\n (level: RecordedLog['level']) =>\n (context: Record<string, unknown>, message: string) => {\n logs.push({ level, context, message });\n };\n const logger: IntegrationLogger = {\n debug: record('debug'),\n info: record('info'),\n warn: record('warn'),\n error: record('error'),\n };\n\n return {\n accountId: options.accountId ?? 'mock-account-id',\n accountSlug: options.accountSlug ?? null,\n userId: actor.kind === 'machine' ? null : userId,\n actor,\n sourceId: options.sourceId ?? null,\n integration: {\n slug: options.integration.slug,\n productId: options.integration.productId ?? 'mock-product-id',\n },\n timezone: options.timezone ?? null,\n storage,\n secrets,\n fetch,\n logger,\n fetchCalls,\n logs,\n dumpStorage() {\n return { account: dumpScope(accountRows), user: dumpScope(userRows) };\n },\n };\n}\n"]}
|
|
1
|
+
{"version":3,"file":"mock-context.js","sourceRoot":"","sources":["../../src/testing/mock-context.ts"],"names":[],"mappings":"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","sourcesContent":["import { PARTNER_STORAGE_LIMITS } from '@ekanos/integration-schema';\n/**\n * `createMockContext()` — the day-1 in-memory `IntegrationContext` for the\n * partner harness (proposal §4, amended by rulings 1–4). Self-contained:\n * nothing here touches Supabase, Vault, or the network. Enforcement is the\n * SAME shared implementation the production wrapper uses\n * (`@ekanos/sdk/context`): schema validation via `parseStorageValue`, egress\n * via `isEgressAllowed` — a test that passes the mock cannot be a schema or\n * egress surprise later.\n */\nimport type { z } from 'zod';\n\nimport { isEgressAllowed } from '../context/egress';\nimport {\n EgressDeniedError,\n SecretAccessError,\n StorageValidationError,\n} from '../context/errors';\nimport {\n assertStorageKeyDeclared,\n assertStorageKeyWritable,\n parseStorageValue,\n} from '../context/storage-validation';\nimport type {\n IntegrationActor,\n IntegrationContext,\n IntegrationFetch,\n IntegrationLogger,\n IntegrationSecrets,\n IntegrationStorage,\n ScopedStore,\n StorageEntry,\n StorageKeySchema,\n StorageSchemaMap,\n StorageSchemas,\n StorageWriteOptions,\n} from '../context/types';\nimport { type MockFetchHandler, matchFetchHandler } from './fetch-matching';\n\ntype StorageScope = 'account' | 'user';\n\n// Seeds are typed off the DECLARED schema, resolved out of whichever\n// declaration form the key used — a bare zod schema or a\n// `{ schema, clientReadable }` descriptor.\ntype ScopeSeed<M> = M extends StorageSchemaMap\n ? { [K in keyof M & string]?: z.input<StorageKeySchema<M[K]>> }\n : Record<never, never>;\n\n// Re-exported from here for backward compatibility — `MockFetchHandler`'s\n// canonical home is now `./fetch-matching`, shared with `createMockFetchProvider`.\nexport type { MockFetchHandler } from './fetch-matching';\n\nexport interface MockContextOptions<\n Schemas extends StorageSchemas = StorageSchemas,\n> {\n accountId?: string;\n accountSlug?: string | null;\n userId?: string | null;\n actor?: IntegrationActor;\n sourceId?: string | null;\n timezone?: string | null;\n integration: { slug: string; productId?: string };\n /**\n * The storage declaration `defineIntegration({ storage })` will carry —\n * the mock enforces the identical schemas via the same shared validator\n * (ruling 1). Omit it and every storage access throws.\n */\n storageSchemas?: Schemas;\n /** Seed rows: key → data, per scope. Validated on construction via the write path. */\n storage?: {\n account?: ScopeSeed<Schemas['account']>;\n user?: ScopeSeed<Schemas['user']>;\n };\n /**\n * Seed secrets, by tier (ruling 2): `account` values are the rows this\n * integration stored — writable via `secrets.set()`; `admin` values stand\n * in for admin-issued source/global-tier credentials — readable, and a\n * `set()` against a name resolvable only there throws `SecretAccessError`.\n */\n secrets?: {\n account?: Record<string, string>;\n admin?: Record<string, string>;\n };\n /** Same allowlist the manifest would declare — enforced by the stub. */\n egress?: readonly string[];\n /** Programmable responses; unmatched allowed calls resolve 200 `{}`. */\n fetchHandlers?: MockFetchHandler[];\n}\n\nexport interface RecordedFetchCall {\n url: string;\n init?: RequestInit;\n denied: boolean;\n}\n\nexport interface RecordedLog {\n level: 'debug' | 'info' | 'warn' | 'error';\n context: Record<string, unknown>;\n message: string;\n}\n\nexport interface MockIntegrationContext<\n Schemas extends StorageSchemas = StorageSchemas,\n> extends IntegrationContext<Schemas> {\n /** Every ctx.fetch call, allowed or denied, in order. */\n readonly fetchCalls: readonly RecordedFetchCall[];\n /** Every log line, per level. */\n readonly logs: readonly RecordedLog[];\n /** Current readable storage state (expired entries excluded), key → data. */\n dumpStorage(): {\n account: Record<string, unknown>;\n user: Record<string, unknown>;\n };\n}\n\ninterface StoredRow {\n data: unknown;\n externalId: string | null;\n expiresAt: string | null;\n updatedAt: string;\n}\n\nfunction isExpired(row: StoredRow): boolean {\n return row.expiresAt !== null && Date.parse(row.expiresAt) <= Date.now();\n}\n\nfunction parseExpiresAt(\n options: StorageWriteOptions | undefined,\n): string | null {\n const expiresAt = options?.expiresAt;\n if (expiresAt === undefined) return null;\n if (Number.isNaN(Date.parse(expiresAt))) {\n throw new TypeError(\n `expiresAt \"${expiresAt}\" is not a parseable timestamp. Pass an ` +\n `ISO-8601 string, e.g. new Date(Date.now() + 60_000).toISOString().`,\n );\n }\n return expiresAt;\n}\n\nfunction createInMemoryStore(args: {\n scope: StorageScope;\n schemas: StorageSchemaMap | undefined;\n rows: Map<string, StoredRow>;\n guard: () => void;\n}): ScopedStore {\n const { scope, schemas, rows, guard } = args;\n\n return {\n async get(key: string): Promise<StorageEntry<unknown> | null> {\n guard();\n assertStorageKeyDeclared({ scope, key, schemas });\n const row = rows.get(key);\n if (!row) return null;\n if (isExpired(row)) {\n rows.delete(key);\n return null;\n }\n const data = parseStorageValue({\n scope,\n key,\n operation: 'read',\n schemas,\n value: row.data,\n });\n return {\n data,\n externalId: row.externalId,\n expiresAt: row.expiresAt,\n updatedAt: row.updatedAt,\n };\n },\n\n async set(\n key: string,\n data: unknown,\n options?: StorageWriteOptions,\n ): Promise<void> {\n guard();\n assertStorageKeyWritable({ scope, key });\n const parsed = parseStorageValue({\n scope,\n key,\n operation: 'write',\n schemas,\n value: data,\n });\n // The host's per-value cap (finding E), enforced here with the host's\n // own wording so `ekanos test` fails where production would.\n const serializedBytes = utf8Bytes(JSON.stringify(parsed));\n if (serializedBytes > PARTNER_STORAGE_LIMITS.maxSerializedValueBytes) {\n throw new StorageValidationError({\n scope,\n key,\n hint:\n `The value written to ${scope} storage key \"${key}\" serializes ` +\n `to ${serializedBytes} bytes, above the ` +\n `${PARTNER_STORAGE_LIMITS.maxSerializedValueBytes}-byte per-value ` +\n `limit. Store less per key: trim cached payloads, or split state ` +\n `across purpose-named keys.`,\n });\n }\n rows.set(key, {\n data: parsed,\n externalId: options?.externalId ?? null,\n expiresAt: parseExpiresAt(options),\n updatedAt: new Date().toISOString(),\n });\n },\n\n async delete(key: string): Promise<void> {\n guard();\n assertStorageKeyWritable({ scope, key });\n assertStorageKeyDeclared({ scope, key, schemas });\n rows.delete(key);\n },\n };\n}\n\n/** Byte length of a string as UTF-8, without Buffer (this module runs in the browser-side harness too). */\nfunction utf8Bytes(value: string): number {\n return new TextEncoder().encode(value).length;\n}\n\nfunction dumpScope(rows: Map<string, StoredRow>): Record<string, unknown> {\n const out: Record<string, unknown> = {};\n for (const [key, row] of rows) {\n if (!isExpired(row)) out[key] = row.data;\n }\n return out;\n}\n\nexport function createMockContext<\n Schemas extends StorageSchemas = StorageSchemas,\n>(options: MockContextOptions<Schemas>): MockIntegrationContext<Schemas> {\n const userId = options.userId !== undefined ? options.userId : 'mock-user-id';\n const actor: IntegrationActor =\n options.actor ??\n (userId !== null\n ? { kind: 'user', userId }\n : { kind: 'machine', tokenId: 'mock-token-id', createdBy: null });\n\n const schemas: StorageSchemas = options.storageSchemas ?? {};\n\n const accountRows = new Map<string, StoredRow>();\n const userRows = new Map<string, StoredRow>();\n\n const seedScope = (\n scope: StorageScope,\n rows: Map<string, StoredRow>,\n seed: Record<string, unknown> | undefined,\n ) => {\n for (const [key, value] of Object.entries(seed ?? {})) {\n const parsed = parseStorageValue({\n scope,\n key,\n operation: 'write',\n schemas: schemas[scope],\n value,\n });\n rows.set(key, {\n data: parsed,\n externalId: null,\n expiresAt: null,\n updatedAt: new Date().toISOString(),\n });\n }\n };\n seedScope('account', accountRows, options.storage?.account);\n seedScope('user', userRows, options.storage?.user);\n\n const guardUserScope = () => {\n if (actor.kind === 'machine') {\n throw new Error(\n 'ctx.storage.user is unavailable for machine actors: there is no ' +\n 'user to scope the row to. Store machine-written state in ' +\n 'ctx.storage.account instead.',\n );\n }\n };\n\n // The stores validate every key and value at runtime via the shared\n // validator; the cast narrows the runtime-checked store to the\n // schema-typed interface the declaration promises.\n const storage = {\n account: createInMemoryStore({\n scope: 'account',\n schemas: schemas.account,\n rows: accountRows,\n guard: () => void 0,\n }),\n user: createInMemoryStore({\n scope: 'user',\n schemas: schemas.user,\n rows: userRows,\n guard: guardUserScope,\n }),\n } as unknown as IntegrationStorage<Schemas>;\n\n const accountSecrets = new Map(\n Object.entries(options.secrets?.account ?? {}),\n );\n const adminSecrets = new Map(Object.entries(options.secrets?.admin ?? {}));\n\n const secrets: IntegrationSecrets = {\n async get(name) {\n return accountSecrets.get(name) ?? adminSecrets.get(name) ?? null;\n },\n async names() {\n return [\n ...new Set([...accountSecrets.keys(), ...adminSecrets.keys()]),\n ].sort();\n },\n async set(name, value) {\n if (!accountSecrets.has(name) && adminSecrets.has(name)) {\n throw new SecretAccessError(name);\n }\n // The host's caps (finding E): one Vault record per value, one row +\n // record per distinct name. Same wording as the host.\n const valueBytes = utf8Bytes(value);\n if (valueBytes > PARTNER_STORAGE_LIMITS.maxSerializedValueBytes) {\n throw new TypeError(\n `secrets.set(\"${name}\", …): the value is ${valueBytes} bytes, ` +\n `above the ${PARTNER_STORAGE_LIMITS.maxSerializedValueBytes}-byte ` +\n `per-secret limit. Secrets hold credentials, not payloads — store ` +\n `bulk data under a ctx.storage key instead.`,\n );\n }\n if (\n !accountSecrets.has(name) &&\n accountSecrets.size >= PARTNER_STORAGE_LIMITS.maxSecretNames\n ) {\n throw new Error(\n `secrets.set(\"${name}\", …): this integration already stores ` +\n `${PARTNER_STORAGE_LIMITS.maxSecretNames} distinct secret names, ` +\n `the per-integration limit. Reuse or rotate an existing name.`,\n );\n }\n accountSecrets.set(name, value);\n },\n };\n\n const fetchCalls: RecordedFetchCall[] = [];\n const egress = options.egress ?? [];\n const fetchHandlers = options.fetchHandlers ?? [];\n\n // Not an async function: a denied call must throw synchronously, before\n // any I/O (proposal §2.3) — same behavior the production wrapper has.\n const fetch: IntegrationFetch = (input, init) => {\n const url = new URL(input).toString();\n const allowed = isEgressAllowed(url, egress);\n fetchCalls.push(\n init === undefined\n ? { url, denied: !allowed }\n : { url, init, denied: !allowed },\n );\n if (!allowed) {\n throw new EgressDeniedError(new URL(url).origin);\n }\n const handler = matchFetchHandler(url, fetchHandlers);\n if (handler) {\n return Promise.resolve(handler.respond(new Request(url, init)));\n }\n return Promise.resolve(\n new Response('{}', {\n status: 200,\n headers: { 'content-type': 'application/json' },\n }),\n );\n };\n\n const logs: RecordedLog[] = [];\n const record =\n (level: RecordedLog['level']) =>\n (context: Record<string, unknown>, message: string) => {\n logs.push({ level, context, message });\n };\n const logger: IntegrationLogger = {\n debug: record('debug'),\n info: record('info'),\n warn: record('warn'),\n error: record('error'),\n };\n\n return {\n accountId: options.accountId ?? 'mock-account-id',\n accountSlug: options.accountSlug ?? null,\n userId: actor.kind === 'machine' ? null : userId,\n actor,\n sourceId: options.sourceId ?? null,\n integration: {\n slug: options.integration.slug,\n productId: options.integration.productId ?? 'mock-product-id',\n },\n timezone: options.timezone ?? null,\n storage,\n secrets,\n fetch,\n logger,\n fetchCalls,\n logs,\n dumpStorage() {\n return { account: dumpScope(accountRows), user: dumpScope(userRows) };\n },\n };\n}\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mock-fetch-provider.js","sourceRoot":"","sources":["../../src/testing/mock-fetch-provider.tsx"],"names":[],"mappings":"AAAA,YAAY,CAAC;;AAKb,OAAO,EAAE,wBAAwB,EAAE,MAAM,oCAAoC,CAAC;AAC9E,OAAO,EAAyB,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AAE5E;;;;;;;;;;;;;;;GAeG;AACH,MAAM,UAAU,uBAAuB,CAAC,QAA4B;IAClE,MAAM,KAAK,GAAqB,KAAK,EAAE,KAAK,EAAE,IAAI,EAAE,EAAE;QACpD,MAAM,GAAG,GAAG,OAAO,KAAK,KAAK,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,QAAQ,EAAE,CAAC;QACjE,MAAM,OAAO,GAAG,iBAAiB,CAAC,GAAG,EAAE,QAAQ,CAAC,CAAC;QACjD,MAAM,OAAO,GAAG,IAAI,OAAO,CAAC,YAAY,CAAC,GAAG,CAAC,EAAE,IAAI,CAAC,CAAC;QAErD,IAAI,OAAO;YAAE,OAAO,OAAO,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC;QAE7C,OAAO,IAAI,QAAQ,CAAC,IAAI,EAAE;YACxB,MAAM,EAAE,GAAG;YACX,OAAO,EAAE,EAAE,cAAc,EAAE,kBAAkB,EAAE;SAChD,CAAC,CAAC;IACL,CAAC,CAAC;IAEF,OAAO,SAAS,4BAA4B,CAAC,EAC3C,QAAQ,GAGT;QACC,OAAO,CACL,KAAC,wBAAwB,IAAC,KAAK,EAAE,KAAK,YACnC,QAAQ,GACgB,CAC5B,CAAC;IACJ,CAAC,CAAC;AACJ,CAAC;AAED,8EAA8E;AAC9E,SAAS,YAAY,CAAC,GAAW;IAC/B,IAAI,CAAC;QACH,OAAO,IAAI,GAAG,CAAC,GAAG,CAAC,CAAC,QAAQ,EAAE,CAAC;IACjC,CAAC;
|
|
1
|
+
{"version":3,"file":"mock-fetch-provider.js","sourceRoot":"","sources":["../../src/testing/mock-fetch-provider.tsx"],"names":[],"mappings":"AAAA,YAAY,CAAC;;AAKb,OAAO,EAAE,wBAAwB,EAAE,MAAM,oCAAoC,CAAC;AAC9E,OAAO,EAAyB,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AAE5E;;;;;;;;;;;;;;;GAeG;AACH,MAAM,UAAU,uBAAuB,CAAC,QAA4B;IAClE,MAAM,KAAK,GAAqB,KAAK,EAAE,KAAK,EAAE,IAAI,EAAE,EAAE;QACpD,MAAM,GAAG,GAAG,OAAO,KAAK,KAAK,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,QAAQ,EAAE,CAAC;QACjE,MAAM,OAAO,GAAG,iBAAiB,CAAC,GAAG,EAAE,QAAQ,CAAC,CAAC;QACjD,MAAM,OAAO,GAAG,IAAI,OAAO,CAAC,YAAY,CAAC,GAAG,CAAC,EAAE,IAAI,CAAC,CAAC;QAErD,IAAI,OAAO;YAAE,OAAO,OAAO,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC;QAE7C,OAAO,IAAI,QAAQ,CAAC,IAAI,EAAE;YACxB,MAAM,EAAE,GAAG;YACX,OAAO,EAAE,EAAE,cAAc,EAAE,kBAAkB,EAAE;SAChD,CAAC,CAAC;IACL,CAAC,CAAC;IAEF,OAAO,SAAS,4BAA4B,CAAC,EAC3C,QAAQ,GAGT;QACC,OAAO,CACL,KAAC,wBAAwB,IAAC,KAAK,EAAE,KAAK,YACnC,QAAQ,GACgB,CAC5B,CAAC;IACJ,CAAC,CAAC;AACJ,CAAC;AAED,8EAA8E;AAC9E,SAAS,YAAY,CAAC,GAAW;IAC/B,IAAI,CAAC;QACH,OAAO,IAAI,GAAG,CAAC,GAAG,CAAC,CAAC,QAAQ,EAAE,CAAC;IACjC,CAAC;eAAO,CAAC;QACP,OAAO,IAAI,GAAG,CAAC,GAAG,EAAE,kBAAkB,CAAC,CAAC,QAAQ,EAAE,CAAC;IACrD,CAAC;AACH,CAAC","sourcesContent":["'use client';\n\nimport type { ReactNode } from 'react';\n\nimport type { IntegrationFetch } from '../context/types';\nimport { IntegrationFetchProvider } from '../hooks/integration-fetch-context';\nimport { type MockFetchHandler, matchFetchHandler } from './fetch-matching';\n\n/**\n * Mirrors `createMockContext`'s `fetchHandlers` — same `MockFetchHandler`\n * shape, same string-prefix/RegExp matching, same unmatched-request fallback\n * (a plausible empty 200) — so a widget test and an `onActivate`/handler test\n * written against the same fixture describe the same vendor behavior once.\n * No egress check here: a widget under test isn't exercising the allowlist,\n * it's exercising its own rendering against a canned response.\n *\n * Widgets often call same-origin host routes with a relative path (e.g.\n * `/api/integrations/<slug>/storage`), not just absolute vendor URLs — unlike\n * `ctx.fetch`, which only ever sees the latter. `new URL(input)` throws on a\n * bare relative string, so matching happens against the caller's original\n * `input` verbatim (a relative `match` pattern keeps working), and a\n * relative input is resolved against a dummy origin only to build the\n * `Request` object `respond()` receives.\n */\nexport function createMockFetchProvider(handlers: MockFetchHandler[]) {\n const fetch: IntegrationFetch = async (input, init) => {\n const url = typeof input === 'string' ? input : input.toString();\n const handler = matchFetchHandler(url, handlers);\n const request = new Request(toRequestUrl(url), init);\n\n if (handler) return handler.respond(request);\n\n return new Response('{}', {\n status: 200,\n headers: { 'content-type': 'application/json' },\n });\n };\n\n return function MockIntegrationFetchProvider({\n children,\n }: {\n children: ReactNode;\n }) {\n return (\n <IntegrationFetchProvider fetch={fetch}>\n {children}\n </IntegrationFetchProvider>\n );\n };\n}\n\n/** `Request` requires an absolute URL; a relative one gets a dummy origin. */\nfunction toRequestUrl(url: string): string {\n try {\n return new URL(url).toString();\n } catch {\n return new URL(url, 'http://localhost').toString();\n }\n}\n"]}
|
|
@@ -63,6 +63,14 @@ export type ActivationFormProps = IntegrationComponentProps & {
|
|
|
63
63
|
isLoading?: boolean;
|
|
64
64
|
/** Render inline instead of in a dialog */
|
|
65
65
|
inline?: boolean;
|
|
66
|
+
/**
|
|
67
|
+
* The integration is already connected and the person chose to connect it
|
|
68
|
+
* again — after a token expired, a password changed, or a health check
|
|
69
|
+
* failed. An OAuth form sends them through sign-in even when a token is
|
|
70
|
+
* stored; a credential form simply accepts new values. Activating again
|
|
71
|
+
* resets the integration's health to unknown until the next check.
|
|
72
|
+
*/
|
|
73
|
+
reconnect?: boolean;
|
|
66
74
|
/**
|
|
67
75
|
* The TEAM account context the user is activating within — separate from
|
|
68
76
|
* `accountId`, which for `scope: 'user'` integrations is collapsed to the
|
|
@@ -123,29 +131,27 @@ export interface IntegrationMetadata {
|
|
|
123
131
|
*/
|
|
124
132
|
credentialModel?: 'account' | 'user' | 'source';
|
|
125
133
|
/**
|
|
126
|
-
*
|
|
127
|
-
*
|
|
128
|
-
*
|
|
129
|
-
* INTO feature treatment.
|
|
134
|
+
* Which KIND of integration this is. **Required** — nothing is inferred from
|
|
135
|
+
* omission. An Integration is any package extending `BaseIntegration`; its
|
|
136
|
+
* kind says who turns it on and whether it needs credentials:
|
|
130
137
|
*
|
|
131
|
-
* '
|
|
132
|
-
*
|
|
133
|
-
*
|
|
134
|
-
* `getWorkspaceTargets()
|
|
135
|
-
*
|
|
136
|
-
*
|
|
137
|
-
*
|
|
138
|
-
*
|
|
139
|
-
*
|
|
138
|
+
* 'feature' — an Ekanos-native capability. Enabled for a Source by a
|
|
139
|
+
* super-admin; no third-party credentials, no activation flow, no
|
|
140
|
+
* marketplace tile. Always declares its own top-level sidebar Section
|
|
141
|
+
* via `getSidebarNav()` and NEVER declares `getWorkspaceTargets()` —
|
|
142
|
+
* its widgets have no Workspace of their own, so an admin places them
|
|
143
|
+
* on one in the composer (`/admin/sources/[id]/layout`).
|
|
144
|
+
* 'integration' — a connection to an outside system. Made available to a
|
|
145
|
+
* Source by a super-admin, then connected by a team or user (usually
|
|
146
|
+
* OAuth/SSO); has credentials, an activation flow and a marketplace
|
|
147
|
+
* tile. Declares a Workspace via `getWorkspaceTargets()`, a top-level
|
|
148
|
+
* Section via `getSidebarNav()`, or both (Centrex does both).
|
|
140
149
|
*
|
|
141
|
-
* The
|
|
142
|
-
* declare
|
|
143
|
-
*
|
|
144
|
-
* composer (`/admin/sources/[id]/layout`) rather than onto a workspace
|
|
145
|
-
* named after itself. A host-side test
|
|
146
|
-
* fails the build on either violation.
|
|
150
|
+
* The asymmetry is one-directional and enforced by the host's CI:
|
|
151
|
+
* a Feature MUST declare a Section and MUST NOT declare a Workspace; a
|
|
152
|
+
* connector MAY declare either or both.
|
|
147
153
|
*/
|
|
148
|
-
kind
|
|
154
|
+
kind: 'feature' | 'integration';
|
|
149
155
|
/**
|
|
150
156
|
* Product slugs that MUST be attached to the same Source for this product to
|
|
151
157
|
* function — a hard dependency, not a suggestion.
|
|
@@ -155,7 +161,7 @@ export interface IntegrationMetadata {
|
|
|
155
161
|
* the dependency in both directions ("Requires X" / "Enables Y").
|
|
156
162
|
*
|
|
157
163
|
* Host CI enforces: every slug is registered, nothing requires itself, no
|
|
158
|
-
* cycles, and a `kind: 'feature'` may require
|
|
164
|
+
* cycles, and a `kind: 'feature'` may require a `'integration'` but never
|
|
159
165
|
* the reverse.
|
|
160
166
|
*/
|
|
161
167
|
requires?: string[];
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"integration.js","sourceRoot":"","sources":["../../src/types/integration.ts"],"names":[],"mappings":"","sourcesContent":["import type { ComponentType } from 'react';\n\nimport type { IconRenderer } from './icon';\n\n// Capability shown on the integration detail page (numbered grid)\nexport interface IntegrationCapability {\n label: string;\n description: string;\n icon?: IconRenderer;\n}\n\n// Permission scope shown on the integration detail page\nexport interface IntegrationPermission {\n label: string;\n detail: string;\n type: 'read' | 'write';\n}\n\n// ---- Integration Health Monitoring (issue #474) -----------------------------\n\n// Higher = healthier. Used internally for \"least healthy wins\" merging.\nexport type HealthStatus = 'healthy' | 'degraded' | 'unhealthy' | 'unknown';\n\n/**\n * Result returned by `BaseIntegration.healthCheck(accountId)`. Routed through\n * `classifyHealth()` (server-side) before writing to\n * `integration_health_status`.\n */\nexport interface HealthCheckResult {\n status: HealthStatus;\n errorCode?: string;\n errorMessage?: string;\n /** Rolling 24h error rate from `account_product_usage_events`, 0..100. */\n errorRatePct?: number;\n /** Sample size used to compute errorRatePct; if below threshold, treated as unknown. */\n sampleSize?: number;\n /** Whether the most recent observed call succeeded. */\n lastObservationSucceeded?: boolean;\n /** Token/credential expiry, if known. Drives token-expiry warnings. */\n tokenExpiresAt?: string;\n /** Free-form per-integration data carried to the dashboard. */\n metadata?: Record<string, unknown>;\n}\n\n// Activation result from integration\nexport interface ActivationResult {\n success: boolean;\n configData?: Record<string, unknown>;\n externalId?: string;\n errorMessage?: string;\n metadata?: Record<string, unknown>;\n}\n\n// Base props that all integration components need\ninterface BaseIntegrationComponentProps {\n accountId: string;\n sourceId?: string;\n integrationConfig?: Record<string, unknown>;\n}\n\n// Discriminated union for product reference - only one allowed\ntype ProductReference =\n | { productId: string; productSlug?: never }\n | { productSlug: string; productId?: never };\n\n// New base type using discriminated union\nexport type IntegrationComponentProps = BaseIntegrationComponentProps &\n ProductReference;\n\n// Marketplace tile props\nexport type MarketplaceTileProps = IntegrationComponentProps & {\n onActivate?: () => void;\n isActivated?: boolean;\n /** Navigation URL for the integration detail page */\n href?: string;\n};\n\n// Activation form props\nexport type ActivationFormProps = IntegrationComponentProps & {\n onSuccess?: (result: ActivationResult) => void;\n onCancel?: () => void;\n isLoading?: boolean;\n /** Render inline instead of in a dialog */\n inline?: boolean;\n /**\n * The TEAM account context the user is activating within — separate from\n * `accountId`, which for `scope: 'user'` integrations is collapsed to the\n * personal account for activation storage/token purposes. Only consumed by\n * workspace materialization (`materializeWorkspaceOnActivation`), never for\n * activation/token storage itself (plan 009 Part A2). Optional: undefined\n * for team-scoped integrations (their `accountId` already IS the team) and\n * for any caller that hasn't threaded it yet — the server action falls back\n * to `accountId` when absent.\n */\n teamAccountId?: string;\n};\n\n// Integration metadata\nexport interface IntegrationMetadata {\n productId: string;\n productSlug: string;\n integrationName: string;\n description: string;\n category: string;\n version: string;\n icon?: string;\n /** Bundled logo URL — overrides product.logo_path on the marketplace */\n logoPath?: string;\n /** When 'user', activation data is stored under the user's personal account (user.id) instead of the team account */\n scope?: 'user' | 'team';\n /**\n * Set FALSE to assert this integration has NO self-serve activation surface —\n * the marketplace detail page then renders no activation form at all.\n *\n * This is deliberately distinct from \"has no CUSTOM activation form\": an\n * integration that omits `getActivationForm()` still gets the generic one, and\n * six integrations depend on that. Use this flag only when submitting ANY\n * activation form would be a dead end — where `activate()` cannot succeed, so\n * showing a form hands the user a button that cannot work.\n * Undefined/true = normal self-serve activation.\n */\n selfServeActivation?: boolean;\n /**\n * How this integration's MCP tools resolve credentials — the integration's\n * SELF-DECLARATION of machine-executability, consumed dynamically by the\n * workflow engine's fail-closed tool allowlist (isGatewayExposableTool).\n *\n * 'account' — credentials/identity resolve from team- or source-level\n * config via the ToolContext (`getAccountId(ctx)` + `ctx.supabase`).\n * Safe for durable machine-principal execution (workflows): eligible\n * tools (read + public/internal sensitivity) become workflow-callable.\n * 'user' — credentials or identity are personal (ctx.user.id, personal\n * OAuth, session reads like getSupabaseServerClient()/requireUser()).\n * NEVER machine-executable: a workflow would silently ride one\n * employee's personal grant.\n * 'source' — platform-level credentials (diagnostics-class integrations).\n * Not exposed to account workflows.\n *\n * UNDECLARED = fail closed (treated as not machine-executable). Declaring\n * 'account' is a security assertion: the integration's MCP run() handlers\n * must use ctx-based resolution ONLY — no session clients, no personal\n * grants. A guard test in @kit/integrations-workflows enforces the obvious\n * violations (session-client reads in mcp/ code of 'account' declarers).\n */\n credentialModel?: 'account' | 'user' | 'source';\n /**\n * What KIND of thing this integration is — the platform's presentation and\n * governance discriminant. Undefined means `'integration'`, which is what\n * every third-party connector is; the field only ever needs setting to opt\n * INTO feature treatment.\n *\n * 'integration' — connects Fusion to a third party. Has credentials, an\n * activation flow, and a marketplace presence. Its widgets live on\n * workspace dashboards and it declares its workspaces via\n * `getWorkspaceTargets()`.\n * 'feature' — a Fusion-native capability with a bespoke experience: no\n * third-party credentials, its own route tree under\n * `/home/[account]/…`, and its own sidebar section declared via\n * `getSidebarNav()` (optionally rendered by its own\n * `getSidebarComponent()`).\n *\n * The two are mutually exclusive on the nav question. A `'feature'` MUST\n * declare `getSidebarNav()` and MUST NOT declare `getWorkspaceTargets()` —\n * its widgets are placed onto whatever workspace an admin chooses in the\n * composer (`/admin/sources/[id]/layout`) rather than onto a workspace\n * named after itself. A host-side test\n * fails the build on either violation.\n */\n kind?: 'integration' | 'feature';\n /**\n * Product slugs that MUST be attached to the same Source for this product to\n * function — a hard dependency, not a suggestion.\n *\n * The host REFUSES to attach this product to a Source missing any of them,\n * and refuses a detach that would strand a dependent. Admin surfaces render\n * the dependency in both directions (\"Requires X\" / \"Enables Y\").\n *\n * Host CI enforces: every slug is registered, nothing requires itself, no\n * cycles, and a `kind: 'feature'` may require an `'integration'` but never\n * the reverse.\n */\n requires?: string[];\n}\n\n// Widget configuration\nexport interface WidgetConfig {\n id: string;\n name: string;\n component: ComponentType<IntegrationComponentProps>;\n gridSize?: { cols: number; rows: number } | { cols: number; rows: number }[];\n gridPosition?: { col: number; row: number };\n widgetState: 'active' | 'inactive' | 'disabled';\n // React-grid-layout format\n layouts?: {\n lg?: { x: number; y: number; w: number; h: number; maxHeight?: number };\n md?: { x: number; y: number; w: number; h: number; maxHeight?: number };\n sm?: { x: number; y: number; w: number; h: number; maxHeight?: number };\n };\n // Integration metadata for widgets\n integrationMetadata?: IntegrationMetadata;\n category?: { id: string; name: string; slug: string; icon: string | null };\n // Database product ID (UUID) from widget_config table\n productId?: string;\n isCollapsible?: boolean;\n // For collapsed state persistence\n collapsed?: boolean;\n widgetConfigId?: string;\n // Resolved workspace placement (explicit widget_config.workspace_id\n // override, else the product's inheriting attachment). Populated by the\n // dashboard widgets loader; null = no workspace (unscoped dashboard).\n workspaceId?: string | null;\n isPinnable?: boolean;\n isPinned?: boolean;\n // When true, the dashboard renders the \"Ask about this\" AI footer bar for\n // this widget. The widget may still pass `askContext` to the wrapper for a\n // rich snapshot; otherwise a minimal default is used.\n aiFooterEnabled?: boolean;\n // Integration health for this widget's product. `unhealthy` downgrades the\n // widget to `disabled`; `degraded` surfaces a stale-data footer.\n health?: {\n status: HealthStatus;\n lastSuccessAt?: string | null;\n errorMessage?: string | null;\n };\n}\n\n// Database types for integration tables.\n//\n// PARTNER-VISIBLE SUBSET: these row types deliberately OMIT `vault_secret_id`.\n// A vault id is a host handle to a decryptable secret; the partner surface\n// never exposes one — `ctx.secrets` fences it off. Host code keeps the full\n// row because it needs the handle; a parity test pins the SDK type as the\n// host's row minus exactly the host-only fields, so a new column cannot drift\n// in here unnoticed.\nexport interface IntegrationConfig {\n id: string;\n account_id: string | null;\n product_id: string;\n config_type: 'api_credentials' | 'activation_data' | 'settings';\n config_data: Record<string, unknown>;\n status: 'active' | 'inactive' | 'pending' | 'error' | 'suspended';\n is_active: boolean;\n created_at: string;\n updated_at: string;\n created_by: string | null;\n updated_by: string | null;\n}\n\n/**\n * The PARTNER-VISIBLE `data_type` values on `account_product_data` — a strict\n * SUBSET of `@kit/integrations-core`'s union.\n *\n * `'secret'` is deliberately absent. Secret rows are host-managed (one row per\n * secret name, value in Vault, `vault_secret_id` on the row), written only by\n * the capability context's `persistSecret` and read only through\n * `ctx.secrets`. Naming the kind here would advertise a row shape partner code\n * must never address: `ctx.storage` rejects it, and core's generic accessors\n * reject it too (`config-utils.ts`, `assertNotHostOnlyDataType`) precisely\n * because a write there would land the plaintext in the `data` column.\n *\n * When you add an integration-specific kind, add it in BOTH places (core owns\n * the full union) and keep this one partner-appropriate. Prefer `dataSubtype`\n * for orthogonal slicing within a kind (e.g. `data_type='cache'`,\n * `data_subtype='profile'`); reserve a new top-level kind for data that has a\n * distinct lifecycle or is queried independently.\n */\nexport type AccountProductDataType =\n | 'activation'\n | 'settings'\n | 'metrics_summary'\n | 'sync_state'\n | 'cache'\n // Integration-specific kinds — extend here when needed.\n | 'cash_forecast' // quickbooks — user-supplied manual cash-forecast estimates\n | 'tax_config'; // quickbooks — per-user federal/state tax rate config\n\nexport interface AccountProductData {\n id: string;\n account_id: string;\n product_id: string;\n data_type: AccountProductDataType;\n data_subtype?: string;\n data: Record<string, unknown>;\n expires_at?: string;\n integration_config_id?: string;\n external_id?: string;\n created_at: string;\n updated_at: string;\n created_by: string | null;\n updated_by: string | null;\n}\n"]}
|
|
1
|
+
{"version":3,"file":"integration.js","sourceRoot":"","sources":["../../src/types/integration.ts"],"names":[],"mappings":"","sourcesContent":["import type { ComponentType } from 'react';\n\nimport type { IconRenderer } from './icon';\n\n// Capability shown on the integration detail page (numbered grid)\nexport interface IntegrationCapability {\n label: string;\n description: string;\n icon?: IconRenderer;\n}\n\n// Permission scope shown on the integration detail page\nexport interface IntegrationPermission {\n label: string;\n detail: string;\n type: 'read' | 'write';\n}\n\n// ---- Integration Health Monitoring (issue #474) -----------------------------\n\n// Higher = healthier. Used internally for \"least healthy wins\" merging.\nexport type HealthStatus = 'healthy' | 'degraded' | 'unhealthy' | 'unknown';\n\n/**\n * Result returned by `BaseIntegration.healthCheck(accountId)`. Routed through\n * `classifyHealth()` (server-side) before writing to\n * `integration_health_status`.\n */\nexport interface HealthCheckResult {\n status: HealthStatus;\n errorCode?: string;\n errorMessage?: string;\n /** Rolling 24h error rate from `account_product_usage_events`, 0..100. */\n errorRatePct?: number;\n /** Sample size used to compute errorRatePct; if below threshold, treated as unknown. */\n sampleSize?: number;\n /** Whether the most recent observed call succeeded. */\n lastObservationSucceeded?: boolean;\n /** Token/credential expiry, if known. Drives token-expiry warnings. */\n tokenExpiresAt?: string;\n /** Free-form per-integration data carried to the dashboard. */\n metadata?: Record<string, unknown>;\n}\n\n// Activation result from integration\nexport interface ActivationResult {\n success: boolean;\n configData?: Record<string, unknown>;\n externalId?: string;\n errorMessage?: string;\n metadata?: Record<string, unknown>;\n}\n\n// Base props that all integration components need\ninterface BaseIntegrationComponentProps {\n accountId: string;\n sourceId?: string;\n integrationConfig?: Record<string, unknown>;\n}\n\n// Discriminated union for product reference - only one allowed\ntype ProductReference =\n | { productId: string; productSlug?: never }\n | { productSlug: string; productId?: never };\n\n// New base type using discriminated union\nexport type IntegrationComponentProps = BaseIntegrationComponentProps &\n ProductReference;\n\n// Marketplace tile props\nexport type MarketplaceTileProps = IntegrationComponentProps & {\n onActivate?: () => void;\n isActivated?: boolean;\n /** Navigation URL for the integration detail page */\n href?: string;\n};\n\n// Activation form props\nexport type ActivationFormProps = IntegrationComponentProps & {\n onSuccess?: (result: ActivationResult) => void;\n onCancel?: () => void;\n isLoading?: boolean;\n /** Render inline instead of in a dialog */\n inline?: boolean;\n /**\n * The integration is already connected and the person chose to connect it\n * again — after a token expired, a password changed, or a health check\n * failed. An OAuth form sends them through sign-in even when a token is\n * stored; a credential form simply accepts new values. Activating again\n * resets the integration's health to unknown until the next check.\n */\n reconnect?: boolean;\n /**\n * The TEAM account context the user is activating within — separate from\n * `accountId`, which for `scope: 'user'` integrations is collapsed to the\n * personal account for activation storage/token purposes. Only consumed by\n * workspace materialization (`materializeWorkspaceOnActivation`), never for\n * activation/token storage itself (plan 009 Part A2). Optional: undefined\n * for team-scoped integrations (their `accountId` already IS the team) and\n * for any caller that hasn't threaded it yet — the server action falls back\n * to `accountId` when absent.\n */\n teamAccountId?: string;\n};\n\n// Integration metadata\nexport interface IntegrationMetadata {\n productId: string;\n productSlug: string;\n integrationName: string;\n description: string;\n category: string;\n version: string;\n icon?: string;\n /** Bundled logo URL — overrides product.logo_path on the marketplace */\n logoPath?: string;\n /** When 'user', activation data is stored under the user's personal account (user.id) instead of the team account */\n scope?: 'user' | 'team';\n /**\n * Set FALSE to assert this integration has NO self-serve activation surface —\n * the marketplace detail page then renders no activation form at all.\n *\n * This is deliberately distinct from \"has no CUSTOM activation form\": an\n * integration that omits `getActivationForm()` still gets the generic one, and\n * six integrations depend on that. Use this flag only when submitting ANY\n * activation form would be a dead end — where `activate()` cannot succeed, so\n * showing a form hands the user a button that cannot work.\n * Undefined/true = normal self-serve activation.\n */\n selfServeActivation?: boolean;\n /**\n * How this integration's MCP tools resolve credentials — the integration's\n * SELF-DECLARATION of machine-executability, consumed dynamically by the\n * workflow engine's fail-closed tool allowlist (isGatewayExposableTool).\n *\n * 'account' — credentials/identity resolve from team- or source-level\n * config via the ToolContext (`getAccountId(ctx)` + `ctx.supabase`).\n * Safe for durable machine-principal execution (workflows): eligible\n * tools (read + public/internal sensitivity) become workflow-callable.\n * 'user' — credentials or identity are personal (ctx.user.id, personal\n * OAuth, session reads like getSupabaseServerClient()/requireUser()).\n * NEVER machine-executable: a workflow would silently ride one\n * employee's personal grant.\n * 'source' — platform-level credentials (diagnostics-class integrations).\n * Not exposed to account workflows.\n *\n * UNDECLARED = fail closed (treated as not machine-executable). Declaring\n * 'account' is a security assertion: the integration's MCP run() handlers\n * must use ctx-based resolution ONLY — no session clients, no personal\n * grants. A guard test in @kit/integrations-workflows enforces the obvious\n * violations (session-client reads in mcp/ code of 'account' declarers).\n */\n credentialModel?: 'account' | 'user' | 'source';\n /**\n * Which KIND of integration this is. **Required** — nothing is inferred from\n * omission. An Integration is any package extending `BaseIntegration`; its\n * kind says who turns it on and whether it needs credentials:\n *\n * 'feature' — an Ekanos-native capability. Enabled for a Source by a\n * super-admin; no third-party credentials, no activation flow, no\n * marketplace tile. Always declares its own top-level sidebar Section\n * via `getSidebarNav()` and NEVER declares `getWorkspaceTargets()` —\n * its widgets have no Workspace of their own, so an admin places them\n * on one in the composer (`/admin/sources/[id]/layout`).\n * 'integration' — a connection to an outside system. Made available to a\n * Source by a super-admin, then connected by a team or user (usually\n * OAuth/SSO); has credentials, an activation flow and a marketplace\n * tile. Declares a Workspace via `getWorkspaceTargets()`, a top-level\n * Section via `getSidebarNav()`, or both (Centrex does both).\n *\n * The asymmetry is one-directional and enforced by the host's CI:\n * a Feature MUST declare a Section and MUST NOT declare a Workspace; a\n * connector MAY declare either or both.\n */\n kind: 'feature' | 'integration';\n /**\n * Product slugs that MUST be attached to the same Source for this product to\n * function — a hard dependency, not a suggestion.\n *\n * The host REFUSES to attach this product to a Source missing any of them,\n * and refuses a detach that would strand a dependent. Admin surfaces render\n * the dependency in both directions (\"Requires X\" / \"Enables Y\").\n *\n * Host CI enforces: every slug is registered, nothing requires itself, no\n * cycles, and a `kind: 'feature'` may require a `'integration'` but never\n * the reverse.\n */\n requires?: string[];\n}\n\n// Widget configuration\nexport interface WidgetConfig {\n id: string;\n name: string;\n component: ComponentType<IntegrationComponentProps>;\n gridSize?: { cols: number; rows: number } | { cols: number; rows: number }[];\n gridPosition?: { col: number; row: number };\n widgetState: 'active' | 'inactive' | 'disabled';\n // React-grid-layout format\n layouts?: {\n lg?: { x: number; y: number; w: number; h: number; maxHeight?: number };\n md?: { x: number; y: number; w: number; h: number; maxHeight?: number };\n sm?: { x: number; y: number; w: number; h: number; maxHeight?: number };\n };\n // Integration metadata for widgets\n integrationMetadata?: IntegrationMetadata;\n category?: { id: string; name: string; slug: string; icon: string | null };\n // Database product ID (UUID) from widget_config table\n productId?: string;\n isCollapsible?: boolean;\n // For collapsed state persistence\n collapsed?: boolean;\n widgetConfigId?: string;\n // Resolved workspace placement (explicit widget_config.workspace_id\n // override, else the product's inheriting attachment). Populated by the\n // dashboard widgets loader; null = no workspace (unscoped dashboard).\n workspaceId?: string | null;\n isPinnable?: boolean;\n isPinned?: boolean;\n // When true, the dashboard renders the \"Ask about this\" AI footer bar for\n // this widget. The widget may still pass `askContext` to the wrapper for a\n // rich snapshot; otherwise a minimal default is used.\n aiFooterEnabled?: boolean;\n // Integration health for this widget's product. `unhealthy` downgrades the\n // widget to `disabled`; `degraded` surfaces a stale-data footer.\n health?: {\n status: HealthStatus;\n lastSuccessAt?: string | null;\n errorMessage?: string | null;\n };\n}\n\n// Database types for integration tables.\n//\n// PARTNER-VISIBLE SUBSET: these row types deliberately OMIT `vault_secret_id`.\n// A vault id is a host handle to a decryptable secret; the partner surface\n// never exposes one — `ctx.secrets` fences it off. Host code keeps the full\n// row because it needs the handle; a parity test pins the SDK type as the\n// host's row minus exactly the host-only fields, so a new column cannot drift\n// in here unnoticed.\nexport interface IntegrationConfig {\n id: string;\n account_id: string | null;\n product_id: string;\n config_type: 'api_credentials' | 'activation_data' | 'settings';\n config_data: Record<string, unknown>;\n status: 'active' | 'inactive' | 'pending' | 'error' | 'suspended';\n is_active: boolean;\n created_at: string;\n updated_at: string;\n created_by: string | null;\n updated_by: string | null;\n}\n\n/**\n * The PARTNER-VISIBLE `data_type` values on `account_product_data` — a strict\n * SUBSET of `@kit/integrations-core`'s union.\n *\n * `'secret'` is deliberately absent. Secret rows are host-managed (one row per\n * secret name, value in Vault, `vault_secret_id` on the row), written only by\n * the capability context's `persistSecret` and read only through\n * `ctx.secrets`. Naming the kind here would advertise a row shape partner code\n * must never address: `ctx.storage` rejects it, and core's generic accessors\n * reject it too (`config-utils.ts`, `assertNotHostOnlyDataType`) precisely\n * because a write there would land the plaintext in the `data` column.\n *\n * When you add an integration-specific kind, add it in BOTH places (core owns\n * the full union) and keep this one partner-appropriate. Prefer `dataSubtype`\n * for orthogonal slicing within a kind (e.g. `data_type='cache'`,\n * `data_subtype='profile'`); reserve a new top-level kind for data that has a\n * distinct lifecycle or is queried independently.\n */\nexport type AccountProductDataType =\n | 'activation'\n | 'settings'\n | 'metrics_summary'\n | 'sync_state'\n | 'cache'\n // Integration-specific kinds — extend here when needed.\n | 'cash_forecast' // quickbooks — user-supplied manual cash-forecast estimates\n | 'tax_config'; // quickbooks — per-user federal/state tax rate config\n\nexport interface AccountProductData {\n id: string;\n account_id: string;\n product_id: string;\n data_type: AccountProductDataType;\n data_subtype?: string;\n data: Record<string, unknown>;\n expires_at?: string;\n integration_config_id?: string;\n external_id?: string;\n created_at: string;\n updated_at: string;\n created_by: string | null;\n updated_by: string | null;\n}\n"]}
|
|
@@ -33,4 +33,13 @@ export interface WidgetAskContext {
|
|
|
33
33
|
* contextual prompts that previously sat on the widget body.
|
|
34
34
|
*/
|
|
35
35
|
suggestedPrompts?: WidgetSuggestedPrompt[];
|
|
36
|
+
/**
|
|
37
|
+
* `send` (the default) starts a fresh thread and sends `suggestedPrompt` at
|
|
38
|
+
* once — a widget's own "Ask about this". `attach` keeps the conversation,
|
|
39
|
+
* sends nothing, and pins this context to the composer as a chip (with
|
|
40
|
+
* `suggestedPrompts` as one-click starts) so it rides on whatever the user
|
|
41
|
+
* types next — for pointing at something to change rather than asking a
|
|
42
|
+
* canned question about it.
|
|
43
|
+
*/
|
|
44
|
+
mode?: 'send' | 'attach';
|
|
36
45
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"widget-ask-context.js","sourceRoot":"","sources":["../../src/types/widget-ask-context.ts"],"names":[],"mappings":"","sourcesContent":["/**\n * A widget snapshot is a JSON-serializable view of what the user currently\n * sees in the widget. It MUST NOT contain React nodes, functions, class\n * instances, credentials, tokens, or internal-only IDs. The chat API route\n * validates the shape before injecting it into the system prompt.\n */\nexport type WidgetSnapshot = Record<string, unknown> | null;\n\n/**\n * A pre-canned prompt rendered as a pill inside the expanded \"Ask about this\"\n * footer. Clicking a suggestion dispatches the prompt to the assistant dock\n * along with the widget's snapshot.\n */\nexport interface WidgetSuggestedPrompt {\n label: string;\n prompt: string;\n}\n\n/**\n * Context a widget passes to the AI assistant when its \"Ask about this\"\n * bar is submitted. The host's bridge attaches `snapshot` to the user\n * message as AI SDK message metadata (NOT as visible chat text). The chat\n * API route injects the snapshot into the system instruction so the model\n * has authoritative widget state; the dock UI renders a small attachment\n * chip above the user bubble using `title` as the label.\n */\nexport interface WidgetAskContext {\n widgetId: string;\n title: string;\n snapshot: WidgetSnapshot;\n suggestedPrompt: string;\n /**\n * Optional pre-canned prompts shown as pills above the input in the\n * expanded \"Ask about this\" footer. Use these to surface the same\n * contextual prompts that previously sat on the widget body.\n */\n suggestedPrompts?: WidgetSuggestedPrompt[];\n}\n"]}
|
|
1
|
+
{"version":3,"file":"widget-ask-context.js","sourceRoot":"","sources":["../../src/types/widget-ask-context.ts"],"names":[],"mappings":"","sourcesContent":["/**\n * A widget snapshot is a JSON-serializable view of what the user currently\n * sees in the widget. It MUST NOT contain React nodes, functions, class\n * instances, credentials, tokens, or internal-only IDs. The chat API route\n * validates the shape before injecting it into the system prompt.\n */\nexport type WidgetSnapshot = Record<string, unknown> | null;\n\n/**\n * A pre-canned prompt rendered as a pill inside the expanded \"Ask about this\"\n * footer. Clicking a suggestion dispatches the prompt to the assistant dock\n * along with the widget's snapshot.\n */\nexport interface WidgetSuggestedPrompt {\n label: string;\n prompt: string;\n}\n\n/**\n * Context a widget passes to the AI assistant when its \"Ask about this\"\n * bar is submitted. The host's bridge attaches `snapshot` to the user\n * message as AI SDK message metadata (NOT as visible chat text). The chat\n * API route injects the snapshot into the system instruction so the model\n * has authoritative widget state; the dock UI renders a small attachment\n * chip above the user bubble using `title` as the label.\n */\nexport interface WidgetAskContext {\n widgetId: string;\n title: string;\n snapshot: WidgetSnapshot;\n suggestedPrompt: string;\n /**\n * Optional pre-canned prompts shown as pills above the input in the\n * expanded \"Ask about this\" footer. Use these to surface the same\n * contextual prompts that previously sat on the widget body.\n */\n suggestedPrompts?: WidgetSuggestedPrompt[];\n /**\n * `send` (the default) starts a fresh thread and sends `suggestedPrompt` at\n * once — a widget's own \"Ask about this\". `attach` keeps the conversation,\n * sends nothing, and pins this context to the composer as a chip (with\n * `suggestedPrompts` as one-click starts) so it rides on whatever the user\n * types next — for pointing at something to change rather than asking a\n * canned question about it.\n */\n mode?: 'send' | 'attach';\n}\n"]}
|
|
@@ -16,8 +16,9 @@ export interface WorkspaceTargetDefinition {
|
|
|
16
16
|
* alphanumeric with single hyphens between segments. */
|
|
17
17
|
slug: string;
|
|
18
18
|
name: string;
|
|
19
|
-
/** A Font Awesome class
|
|
20
|
-
*
|
|
19
|
+
/** A Font Awesome class string, e.g. `fa-light fa-inbox`. Legacy Lucide
|
|
20
|
+
* names are still translated at render time during the migration, but must
|
|
21
|
+
* not be written. Validated by the host at gate time. */
|
|
21
22
|
icon?: string;
|
|
22
23
|
description?: string;
|
|
23
24
|
/** Defaults to `'masonry'` when omitted. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"workspace-target.js","sourceRoot":"","sources":["../../src/types/workspace-target.ts"],"names":[],"mappings":"","sourcesContent":["/**\n * The contract an integration uses to declare the workspaces it ships with\n * (`BaseIntegration.getWorkspaceTargets()`).\n *\n * The host's canonical definition is `z.input<typeof WorkspaceTargetSchema>`\n * (`@kit/integrations-core`, `schemas/workspace-target.schema.ts`) — the\n * schema validates declarations in CI and at materialization time, including\n * cross-field rules a type cannot carry (icon-name validity, slug format,\n * `inheritsWidgets`/`includedWidgetIds` exclusivity, one inheriting target\n * per set). This structural twin exists so the SDK's root entrypoint owns its\n * types without importing the host schema; the parity test beside these types\n * keeps the two shapes mutually assignable.\n */\nexport interface WorkspaceTargetDefinition {\n /** Stable URL segment — `/workspace/<slug>` routes reference it. Lowercase\n * alphanumeric with single hyphens between segments. */\n slug: string;\n name: string;\n /** A Font Awesome class
|
|
1
|
+
{"version":3,"file":"workspace-target.js","sourceRoot":"","sources":["../../src/types/workspace-target.ts"],"names":[],"mappings":"","sourcesContent":["/**\n * The contract an integration uses to declare the workspaces it ships with\n * (`BaseIntegration.getWorkspaceTargets()`).\n *\n * The host's canonical definition is `z.input<typeof WorkspaceTargetSchema>`\n * (`@kit/integrations-core`, `schemas/workspace-target.schema.ts`) — the\n * schema validates declarations in CI and at materialization time, including\n * cross-field rules a type cannot carry (icon-name validity, slug format,\n * `inheritsWidgets`/`includedWidgetIds` exclusivity, one inheriting target\n * per set). This structural twin exists so the SDK's root entrypoint owns its\n * types without importing the host schema; the parity test beside these types\n * keeps the two shapes mutually assignable.\n */\nexport interface WorkspaceTargetDefinition {\n /** Stable URL segment — `/workspace/<slug>` routes reference it. Lowercase\n * alphanumeric with single hyphens between segments. */\n slug: string;\n name: string;\n /** A Font Awesome class string, e.g. `fa-light fa-inbox`. Legacy Lucide\n * names are still translated at render time during the migration, but must\n * not be written. Validated by the host at gate time. */\n icon?: string;\n description?: string;\n /** Defaults to `'masonry'` when omitted. */\n layout?: 'masonry' | 'two-column';\n /**\n * Whether this workspace receives the product's otherwise-unplaced\n * widgets. Exactly one target per product must set it; mutually exclusive\n * with `includedWidgetIds`.\n */\n inheritsWidgets?: boolean;\n /** Limit this workspace to specific widget ids. */\n includedWidgetIds?: string[];\n}\n"]}
|
package/eslint.preset.mjs
CHANGED
|
@@ -100,6 +100,7 @@ const CTX_SECRETS_REMEDIATION =
|
|
|
100
100
|
* closed separately in `no-restricted-syntax` below, so the list cannot be
|
|
101
101
|
* routed around by building the specifier at runtime.
|
|
102
102
|
*/
|
|
103
|
+
/** @type {Array<{ names: string[]; importNames?: string[]; message: string }>} */
|
|
103
104
|
const BANNED_MODULES = [
|
|
104
105
|
{
|
|
105
106
|
names: ['fs', 'node:fs', 'fs/promises', 'node:fs/promises'],
|
|
@@ -194,12 +195,12 @@ const BANNED_MODULES = [
|
|
|
194
195
|
CTX_FETCH_REMEDIATION,
|
|
195
196
|
},
|
|
196
197
|
{
|
|
197
|
-
names: ['react-i18next'],
|
|
198
|
-
importNames: ['Trans'],
|
|
198
|
+
names: ['react-i18next', 'i18next'],
|
|
199
199
|
message:
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
'namespaces and language the surrounding page already loaded.'
|
|
200
|
+
"Use `Trans` from `@ekanos/ui/trans` instead — it reads the host's " +
|
|
201
|
+
'next-intl messages, so your strings resolve against the same ' +
|
|
202
|
+
'namespaces and language the surrounding page already loaded. The ' +
|
|
203
|
+
'host has no i18next instance.',
|
|
203
204
|
},
|
|
204
205
|
];
|
|
205
206
|
|
|
@@ -229,7 +230,7 @@ const GLOBAL_ESCAPE_OTHER_MEMBERS =
|
|
|
229
230
|
const KIT_PATTERN = {
|
|
230
231
|
group: ['@kit/*', '@kit/*/**'],
|
|
231
232
|
message:
|
|
232
|
-
'`@kit/*` packages are workspace-internal to the
|
|
233
|
+
'`@kit/*` packages are workspace-internal to the Ekanos monorepo and do ' +
|
|
233
234
|
'not exist on any registry, so this import cannot resolve for anyone ' +
|
|
234
235
|
'outside it — including in the clean-room build your submission is gated ' +
|
|
235
236
|
'on. Import UI primitives from `@ekanos/ui/*` (badge, button, card, form, ' +
|
|
@@ -394,8 +395,8 @@ export const capabilityRules = {
|
|
|
394
395
|
* The preset. Spread it AFTER any base config: it deliberately replaces a
|
|
395
396
|
* base `no-restricted-imports` / `no-restricted-globals` /
|
|
396
397
|
* `no-restricted-syntax` setting rather than merging with it (flat config does
|
|
397
|
-
* not merge rule options), and it re-states the one entry the
|
|
398
|
-
* config carries —
|
|
398
|
+
* not merge rule options), and it re-states the one entry the Ekanos base
|
|
399
|
+
* config carries — the i18next ban — in its partner-correct form.
|
|
399
400
|
*/
|
|
400
401
|
export default [
|
|
401
402
|
{
|