okengine 0.15.2 → 0.17.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/manifest.v1.schema.json +21 -2
- package/package.json +1 -1
- package/site/content/docs/elements/clock.mdx +7 -0
- package/site/content/docs/elements/flow.mdx +20 -12
- package/site/content/docs/elements/gate.mdx +108 -3
- package/site/content/docs/elements/signal.mdx +46 -17
- package/site/content/docs/elements/store.mdx +36 -32
- package/site/content/docs/elements/vault.mdx +26 -0
- package/site/content/docs/get-started/project-structure.mdx +4 -4
- package/site/content/docs/reference/cli.md +1 -1
- package/site/content/docs/reference/client.mdx +75 -17
- package/site/content/docs/reference/configuration.mdx +7 -4
- package/site/content/docs/reference/errors.mdx +24 -17
- package/site/content/docs/reference/fx.mdx +39 -22
- package/site/content/docs/reference/security.md +1 -1
- package/src/auth/api-key-sql.test.ts +47 -0
- package/src/auth/api-key-sql.ts +261 -0
- package/src/auth/api-keys.ts +276 -14
- package/src/auth/auth.test.ts +56 -0
- package/src/auth/config.ts +24 -5
- package/src/auth/index.ts +62 -1
- package/src/auth/plugin.ts +6 -1
- package/src/auth/schema.ts +8 -0
- package/src/auth/sessions.ts +18 -0
- package/src/auth/tables.ts +32 -0
- package/src/auth/tenant-config.ts +74 -0
- package/src/auth/tenant-tables.ts +11 -0
- package/src/auth/tenants.test.ts +63 -0
- package/src/auth/tenants.ts +360 -0
- package/src/cli/attach-host-console.test.ts +37 -0
- package/src/cli/attach-host-console.ts +6 -0
- package/src/cli/build.ts +2 -2
- package/src/client/budget.test.ts +1 -1
- package/src/client/create.ts +75 -5
- package/src/client/index.ts +13 -0
- package/src/client/live.test.ts +422 -0
- package/src/client/live.ts +389 -0
- package/src/client/notes-contract.test.ts +41 -0
- package/src/client/types.ts +85 -6
- package/src/client-react/index.ts +85 -1
- package/src/client-react/use-live.test.ts +129 -0
- package/src/compiler/effects-infer.ts +37 -2
- package/src/compiler/extract.test.ts +143 -11
- package/src/compiler/extract.ts +210 -30
- package/src/compiler/fixtures/skyport/src/flows/bookings/index.ts +1 -2
- package/src/compiler/fixtures/skyport.expected.json +2 -3
- package/src/compiler/response.ts +41 -11
- package/src/console/server/access.test.ts +116 -8
- package/src/console/server/access.ts +70 -23
- package/src/console/server/app.ts +45 -11
- package/src/console/server/console.test.ts +6 -8
- package/src/console/server/dev-identities.test.ts +127 -0
- package/src/console/server/dev-identities.ts +229 -0
- package/src/console/server/flows-invoke.test.ts +21 -4
- package/src/console/server/flows.ts +113 -58
- package/src/console/server/gates.test.ts +24 -0
- package/src/console/server/gates.ts +10 -30
- package/src/console/server/index.ts +1 -0
- package/src/console/server/live.test.ts +26 -1
- package/src/console/server/live.ts +4 -2
- package/src/console/server/operator-db.ts +2 -0
- package/src/console/server/state.ts +29 -55
- package/src/console/server/store.test.ts +17 -0
- package/src/console/server/store.ts +43 -1
- package/src/console/ui-next/dist/assets/access-page-CXVWWMmD.js +4 -0
- package/src/console/ui-next/dist/assets/{agent-disclosure-C0X1fbWF.js → agent-disclosure-BP0Y0Sux.js} +1 -1
- package/src/console/ui-next/dist/assets/cache-glyph-B5X-NM0-.js +1 -0
- package/src/console/ui-next/dist/assets/call-pii-button-ChAUiLo9.js +1 -0
- package/src/console/ui-next/dist/assets/collapsible-DGnOM2ph.js +1 -0
- package/src/console/ui-next/dist/assets/copy-inline-button-DBHhgHKP.js +1 -0
- package/src/console/ui-next/dist/assets/detail-header-DhHM1iaZ.js +1 -0
- package/src/console/ui-next/dist/assets/dropdown-menu-_NjTEo5_.js +1 -0
- package/src/console/ui-next/dist/assets/element-icons-BVXtRyd3.js +1 -0
- package/src/console/ui-next/dist/assets/explorer-empty-2uIhBu0_.js +1 -0
- package/src/console/ui-next/dist/assets/flows-page-XDpAJO8f.js +1 -0
- package/src/console/ui-next/dist/assets/{highlighted-json-CS_O8L-r.js → highlighted-json-Awq7gYdu.js} +1 -1
- package/src/console/ui-next/dist/assets/http-method-_UHM2ODJ.js +1 -0
- package/src/console/ui-next/dist/assets/index-BBj2QJCu.js +66 -0
- package/src/console/ui-next/dist/assets/index-Ck88Jmv8.css +2 -0
- package/src/console/ui-next/dist/assets/link-BX6Vqztd.js +1 -0
- package/src/console/ui-next/dist/assets/observability-page-BFaay44m.js +4 -0
- package/src/console/ui-next/dist/assets/replica-lag-DKRrbvdo.js +18 -0
- package/src/console/ui-next/dist/assets/request-meta-DV0ywz7t.js +1 -0
- package/src/console/ui-next/dist/assets/shortcut-JQIZlWfm.js +1 -0
- package/src/console/ui-next/dist/assets/shortcut-keys-DKxNTe_m.js +1 -0
- package/src/console/ui-next/dist/assets/skeleton-D-czQJT6.js +1 -0
- package/src/console/ui-next/dist/assets/store-page-CS5-aETQ.js +41 -0
- package/src/console/ui-next/dist/assets/trace-detail-sheet-B09O8rA6.js +2 -0
- package/src/console/ui-next/dist/assets/tree-expand-toggle-BtyhmWb4.js +55 -0
- package/src/console/ui-next/dist/assets/units-page-Ca2Z-E52.js +1 -0
- package/src/console/ui-next/dist/assets/{use-vault-list-BLbZhzyF.js → use-vault-list-uk4WVboC.js} +1 -1
- package/src/console/ui-next/dist/assets/useMutation-B8EO02Ej.js +1 -0
- package/src/console/ui-next/dist/assets/useRender-BE2A9BWC.js +1 -0
- package/src/console/ui-next/dist/assets/vault-page-BmOeAFwg.js +2 -0
- package/src/console/ui-next/dist/index.html +11 -9
- package/src/console/ui-next/index.html +3 -3
- package/src/console/ui-next/src/client.ts +167 -0
- package/src/console/ui-next/src/components/shell/app-sidebar.tsx +2 -0
- package/src/console/ui-next/src/components/shell/command-items.test.ts +1 -0
- package/src/console/ui-next/src/components/shell/command-items.ts +8 -0
- package/src/console/ui-next/src/components/shell/shell-layout.tsx +2 -1
- package/src/console/ui-next/src/components/ui/sheet-form.tsx +8 -3
- package/src/console/ui-next/src/features/access/access-page.tsx +347 -0
- package/src/console/ui-next/src/features/access/data/use-access.ts +23 -0
- package/src/console/ui-next/src/features/access/detail/access-detail.tsx +552 -0
- package/src/console/ui-next/src/features/access/explorer/access-list.tsx +125 -0
- package/src/console/ui-next/src/features/access/lib/format-when.test.ts +139 -0
- package/src/console/ui-next/src/features/access/lib/format-when.ts +296 -0
- package/src/console/ui-next/src/features/access/lib/scope-groups.test.ts +34 -0
- package/src/console/ui-next/src/features/access/lib/scope-groups.ts +40 -0
- package/src/console/ui-next/src/features/access/sheets/access-allow-fields.tsx +236 -0
- package/src/console/ui-next/src/features/access/sheets/access-create-sheet.tsx +316 -0
- package/src/console/ui-next/src/features/access/sheets/access-edit-sheet.tsx +240 -0
- package/src/console/ui-next/src/features/access/sheets/access-expiry-fields.tsx +66 -0
- package/src/console/ui-next/src/features/access/sheets/access-refresh-sheet.tsx +104 -0
- package/src/console/ui-next/src/features/access/sheets/access-scope-field.tsx +115 -0
- package/src/console/ui-next/src/features/access/sheets/access-secret-sheet.tsx +89 -0
- package/src/console/ui-next/src/features/access/sheets/access-sheet-search.tsx +49 -0
- package/src/console/ui-next/src/features/access/state/access-selection.ts +84 -0
- package/src/console/ui-next/src/features/auth/auth-redirect.test.ts +5 -0
- package/src/console/ui-next/src/features/auth/auth-redirect.ts +18 -2
- package/src/console/ui-next/src/features/flows/fixture.ts +0 -1
- package/src/console/ui-next/src/features/not-found/not-found-page.tsx +1 -1
- package/src/console/ui-next/src/features/units/detail/flow-contract-panel.tsx +5 -1
- package/src/console/ui-next/src/features/units/lib/unit-tree.test.ts +15 -4
- package/src/console/ui-next/src/lib/document-meta.test.ts +1 -0
- package/src/console/ui-next/src/lib/document-meta.ts +2 -1
- package/src/console/ui-next/src/lib/last-module-search.test.ts +1 -0
- package/src/console/ui-next/src/lib/last-module-search.ts +2 -1
- package/src/console/ui-next/src/lib/shortcut.test.ts +2 -0
- package/src/console/ui-next/src/lib/shortcut.ts +3 -0
- package/src/console/ui-next/src/router.tsx +10 -1
- package/src/console/ui-next/ui-next-seed-app-schema.ts +88 -37
- package/src/console/ui-next/ui-next-seed-manifest-surface.ts +2 -9
- package/src/console/ui-next/ui-next-seed-manifest.ts +0 -1
- package/src/console/ui-next/ui-next-seed-store.ts +43 -15
- package/src/console/xss.gate.test.ts +1 -0
- package/src/drivers/index.ts +2 -0
- package/src/drivers/journal-postgres.ts +12 -3
- package/src/drivers/pg-rls.ts +26 -2
- package/src/drivers/pg-vault-rls.ts +68 -0
- package/src/drivers/signal-engine.ts +69 -15
- package/src/drivers/signal-live-iter.ts +65 -0
- package/src/drivers/signal-nats.ts +2 -1
- package/src/drivers/signal-postgres.ts +95 -12
- package/src/drivers/signal-redis.ts +2 -1
- package/src/drivers/signal-retention.ts +64 -0
- package/src/drivers/signal-types.ts +35 -5
- package/src/elements/clock/declare.ts +64 -2
- package/src/elements/clock/reconcile.ts +98 -25
- package/src/elements/clock/runtime.ts +13 -2
- package/src/elements/clock.test.ts +28 -0
- package/src/elements/clock.ts +9 -1
- package/src/elements/gate/declare.ts +2 -0
- package/src/elements/gate/permissions.ts +12 -0
- package/src/elements/gate.ts +6 -1
- package/src/elements/signal/declare.ts +44 -10
- package/src/elements/signal/delivery-modes.test.ts +90 -4
- package/src/elements/signal/order-lifecycle.test.ts +20 -4
- package/src/elements/signal/runtime.ts +42 -0
- package/src/elements/signal.test.ts +21 -8
- package/src/elements/signal.ts +1 -1
- package/src/elements/store/cache.ts +1 -1
- package/src/elements/store/declare.ts +7 -0
- package/src/elements/store/rls-identity.test.ts +29 -0
- package/src/elements/store/rls-identity.ts +3 -0
- package/src/elements/store/runtime.ts +14 -0
- package/src/elements/store/schema-decl.ts +162 -44
- package/src/elements/store/schema-tenant.ts +41 -0
- package/src/elements/store/sql-rls-isolation.test.ts +39 -0
- package/src/elements/store/sql-select.types.test.ts +71 -0
- package/src/elements/store/sql-session.ts +54 -32
- package/src/elements/store.ts +5 -0
- package/src/elements/vault/builtin-adapter.ts +15 -2
- package/src/elements/vault/declare.ts +8 -0
- package/src/elements/vault/runtime.ts +7 -0
- package/src/elements/vault/sql-rls-isolation.test.ts +145 -0
- package/src/elements/vault/storage.ts +9 -0
- package/src/elements/vault/test-helpers.ts +2 -1
- package/src/i18n/catalogs/ar.ts +19 -0
- package/src/i18n/catalogs/en.ts +19 -0
- package/src/index.ts +2 -0
- package/src/kernel/adopt-routes.ts +56 -8
- package/src/kernel/api-key-host-persist.test.ts +143 -0
- package/src/kernel/app-tenant.ts +122 -0
- package/src/kernel/app.ts +196 -57
- package/src/kernel/auth-resolve.ts +13 -5
- package/src/kernel/boot-bind/clock.ts +9 -3
- package/src/kernel/boot.test.ts +4 -4
- package/src/kernel/boot.ts +2 -0
- package/src/kernel/budget.test.ts +1 -1
- package/src/kernel/clock-durable.ts +8 -0
- package/src/kernel/clock-per-tenant-name.ts +5 -0
- package/src/kernel/clock-reconcile.ts +8 -0
- package/src/kernel/errors-live-resume.ts +15 -0
- package/src/kernel/errors-tenant.ts +29 -0
- package/src/kernel/errors.registry.test.ts +31 -3
- package/src/kernel/errors.ts +43 -3
- package/src/kernel/flow.ts +26 -5
- package/src/kernel/fx-auth-keys.test.ts +88 -0
- package/src/kernel/fx-auth-keys.ts +203 -0
- package/src/kernel/fx-auth-tenants.test.ts +87 -0
- package/src/kernel/fx-auth-tenants.ts +286 -0
- package/src/kernel/fx-live-stream.ts +149 -0
- package/src/kernel/fx-live.test.ts +157 -0
- package/src/kernel/fx-runtime.ts +15 -0
- package/src/kernel/fx-tenant-store.ts +213 -0
- package/src/kernel/fx.test.ts +108 -1
- package/src/kernel/fx.ts +206 -28
- package/src/kernel/hooks.ts +2 -2
- package/src/kernel/http-resource.ts +9 -18
- package/src/kernel/index.ts +2 -0
- package/src/kernel/journal.ts +12 -0
- package/src/kernel/live-http.test.ts +78 -0
- package/src/kernel/live-http.ts +114 -0
- package/src/kernel/live-resume.test.ts +125 -0
- package/src/kernel/on.ts +51 -0
- package/src/kernel/pipeline-api-key.test.ts +192 -7
- package/src/kernel/pipeline-tenant.ts +49 -0
- package/src/kernel/pipeline.test.ts +1 -1
- package/src/kernel/pipeline.ts +41 -4
- package/src/kernel/resource-mount.test.ts +10 -27
- package/src/kernel/tenant-resolve.test.ts +101 -0
- package/src/kernel/tenant-resolve.ts +124 -0
- package/src/kernel/tenant-roles.test.ts +87 -0
- package/src/kernel/triggers.ts +59 -21
- package/src/manifest/diff.test.ts +11 -2
- package/src/manifest/diff.ts +53 -11
- package/src/manifest/fixtures/skyport.excerpt.json +1 -1
- package/src/manifest/fixtures/skyport.manifest.json +0 -1
- package/src/manifest/types.ts +58 -6
- package/src/release/build-lib.ts +14 -1
- package/src/release/limits.ts +2 -2
- package/src/release/measure.ts +55 -1
- package/src/client/live-gap.test.ts +0 -35
- package/src/console/ui-next/dist/assets/cache-glyph-F1FI122b.js +0 -1
- package/src/console/ui-next/dist/assets/call-pii-button-DEDCl_j3.js +0 -1
- package/src/console/ui-next/dist/assets/collapsible-LPqGvfoz.js +0 -1
- package/src/console/ui-next/dist/assets/confirm-sheet-3ptDzXbz.js +0 -1
- package/src/console/ui-next/dist/assets/explorer-empty-BvYrygyO.js +0 -1
- package/src/console/ui-next/dist/assets/flows-page-Dluo1Bd7.js +0 -1
- package/src/console/ui-next/dist/assets/http-method--sWDdXSB.js +0 -1
- package/src/console/ui-next/dist/assets/index-Ca3HZMVq.js +0 -66
- package/src/console/ui-next/dist/assets/index-UtTDRQpS.css +0 -2
- package/src/console/ui-next/dist/assets/link-COeyggt-.js +0 -1
- package/src/console/ui-next/dist/assets/observability-page-HK9-BO8z.js +0 -4
- package/src/console/ui-next/dist/assets/replica-lag-DBIFFf7d.js +0 -18
- package/src/console/ui-next/dist/assets/sheet-form-Dnwa5oOR.js +0 -1
- package/src/console/ui-next/dist/assets/shortcut-Cgzuv4k1.js +0 -1
- package/src/console/ui-next/dist/assets/shortcut-keys-CoqwEIi0.js +0 -1
- package/src/console/ui-next/dist/assets/skeleton-BHcfDYcb.js +0 -1
- package/src/console/ui-next/dist/assets/store-page-BSohH3wM.js +0 -41
- package/src/console/ui-next/dist/assets/trace-detail-sheet-CFIGRnnA.js +0 -2
- package/src/console/ui-next/dist/assets/tree-expand-toggle-BgLl34w7.js +0 -54
- package/src/console/ui-next/dist/assets/units-page-B0cFE76A.js +0 -1
- package/src/console/ui-next/dist/assets/vault-page-B1SbYe10.js +0 -2
package/src/kernel/fx.ts
CHANGED
|
@@ -20,6 +20,7 @@ import type {
|
|
|
20
20
|
IndexStoreFxHandle,
|
|
21
21
|
KvStoreDecl,
|
|
22
22
|
KvStoreFxHandle,
|
|
23
|
+
SelectFromBuilder,
|
|
23
24
|
SelectOrderBuilder,
|
|
24
25
|
SqlStoreDecl,
|
|
25
26
|
StoreDecl,
|
|
@@ -36,6 +37,12 @@ import type { VaultActor, VaultAdapter, VaultRuntime } from "../elements/vault.t
|
|
|
36
37
|
import type { ChannelRuntime } from "../elements/channel.ts";
|
|
37
38
|
import type { AiRuntime } from "../elements/ai.ts";
|
|
38
39
|
import { parseDurationMs } from "../elements/clock/duration.ts";
|
|
40
|
+
import type { ApiKeyStore } from "../auth/api-keys.ts";
|
|
41
|
+
import type { FxAuthIdentity, FxAuthKeyMethods } from "./fx-auth-keys.ts";
|
|
42
|
+
import type { FxAuthTenantMethods } from "./fx-auth-tenants.ts";
|
|
43
|
+
import type { TenantStore } from "../auth/tenants.ts";
|
|
44
|
+
import type { SessionCrypto, SessionStore } from "../auth/sessions.ts";
|
|
45
|
+
import type { Manifest } from "../manifest/types.ts";
|
|
39
46
|
import { createCapabilityToken, type CapabilityToken } from "./capability.ts";
|
|
40
47
|
import { createEffectLedger, recordEffect, reversibilityOf, type EffectLedger } from "./effects.ts";
|
|
41
48
|
import { resolveDurationMs } from "./elapsed.ts";
|
|
@@ -73,11 +80,27 @@ async function loadRunsWindow(): Promise<typeof import("../runs/window.ts")> {
|
|
|
73
80
|
return import("../runs/window.ts");
|
|
74
81
|
}
|
|
75
82
|
|
|
83
|
+
/**
|
|
84
|
+
* Sync-load live SSE + Last-Event-ID resume only when `fx.live` runs.
|
|
85
|
+
* A static import would pin `checkLiveResume` / 410 encoding on every
|
|
86
|
+
* createFx — including Store-only `oke()` graphs.
|
|
87
|
+
*/
|
|
88
|
+
function loadFxLiveStream(): typeof import("./fx-live-stream.ts") {
|
|
89
|
+
return lazyRequire(import.meta.dir, ["fx", "live", "stream"].join("-"));
|
|
90
|
+
}
|
|
91
|
+
|
|
76
92
|
/** Resource ref Flows declare to read the Runs store via {@link Fx.runs}. */
|
|
77
93
|
export const RUNS_RESOURCE = "runs";
|
|
78
94
|
|
|
79
95
|
/**
|
|
80
|
-
*
|
|
96
|
+
* Resource ref Flows declare for {@link Fx.auth} key management.
|
|
97
|
+
* Same string as `src/auth/api-keys.ts` — defined here so the HMAC / key-store
|
|
98
|
+
* module stays off the edge and Store-only graphs.
|
|
99
|
+
*/
|
|
100
|
+
export const AUTH_API_KEYS_RESOURCE = "auth:api-keys";
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Capability ref for {@link Fx.deadLetters} / {@link Fx.live} — `signal:<name>`, never a store facet.
|
|
81
104
|
*
|
|
82
105
|
* @param name - Signal name
|
|
83
106
|
*/
|
|
@@ -103,13 +126,10 @@ export function resolveStoreRef(ref: NamedRef | { readonly ref: ResourceRef }):
|
|
|
103
126
|
return ref.name as ResourceRef;
|
|
104
127
|
}
|
|
105
128
|
|
|
129
|
+
export type { FxAuthIdentity } from "./fx-auth-keys.ts";
|
|
130
|
+
|
|
106
131
|
/** Auth principal on the user plane. */
|
|
107
|
-
export interface FxAuth {
|
|
108
|
-
readonly userId: string | null;
|
|
109
|
-
readonly scopes: ReadonlySet<string>;
|
|
110
|
-
/** Whether the identity has completed verification (email / MFA). */
|
|
111
|
-
readonly verified?: boolean;
|
|
112
|
-
}
|
|
132
|
+
export interface FxAuth extends FxAuthIdentity, FxAuthKeyMethods, FxAuthTenantMethods {}
|
|
113
133
|
|
|
114
134
|
/** Operator principal on the Console plane. */
|
|
115
135
|
export interface FxOperator {
|
|
@@ -359,16 +379,46 @@ export interface JsonResult<T = unknown> {
|
|
|
359
379
|
readonly kind?: undefined;
|
|
360
380
|
}
|
|
361
381
|
|
|
362
|
-
/** SSE carrier from {@link FxJson.stream}. */
|
|
382
|
+
/** SSE carrier from {@link FxJson.stream} / {@link Fx.live}. */
|
|
363
383
|
export interface JsonStreamResult {
|
|
364
384
|
readonly [jsonResultBrand]: true;
|
|
365
385
|
readonly kind: "stream";
|
|
366
386
|
readonly status: 200;
|
|
367
|
-
readonly chunks: AsyncIterable<
|
|
387
|
+
readonly chunks: AsyncIterable<unknown>;
|
|
388
|
+
/** Awaited before the 200 SSE body; throws OKE1014 on a missing resume cursor. */
|
|
389
|
+
ready?: () => Promise<void>;
|
|
368
390
|
/** Set by the kernel to commit journal / Runs after the stream settles. */
|
|
369
391
|
finalize?: () => Promise<void>;
|
|
370
392
|
}
|
|
371
393
|
|
|
394
|
+
const sseFrameBrand: unique symbol = Symbol.for("oke.sse.frame");
|
|
395
|
+
|
|
396
|
+
/** One SSE frame — optional `id:` plus JSON `data:`. */
|
|
397
|
+
export interface SseFrame {
|
|
398
|
+
readonly [sseFrameBrand]: true;
|
|
399
|
+
readonly data: unknown;
|
|
400
|
+
readonly id?: string;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/**
|
|
404
|
+
* Brand a stream chunk so {@link encodeSseStream} can emit `id:`.
|
|
405
|
+
*
|
|
406
|
+
* @param data - JSON payload
|
|
407
|
+
* @param id - Optional SSE id
|
|
408
|
+
*/
|
|
409
|
+
export function sseFrame(data: unknown, id?: string): SseFrame {
|
|
410
|
+
return id !== undefined ? { [sseFrameBrand]: true, data, id } : { [sseFrameBrand]: true, data };
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
/**
|
|
414
|
+
* True when `value` is a branded SSE frame.
|
|
415
|
+
*
|
|
416
|
+
* @param value - Unknown
|
|
417
|
+
*/
|
|
418
|
+
export function isSseFrame(value: unknown): value is SseFrame {
|
|
419
|
+
return typeof value === "object" && value !== null && (value as SseFrame)[sseFrameBrand] === true;
|
|
420
|
+
}
|
|
421
|
+
|
|
372
422
|
/** True when `value` is an {@link FxJson} JSON-envelope carrier. */
|
|
373
423
|
export function isJsonResult(value: unknown): value is JsonResult {
|
|
374
424
|
return (
|
|
@@ -422,9 +472,9 @@ export interface FxJson {
|
|
|
422
472
|
withQuery<T>(rows: readonly T[], input: unknown, spec?: QueryPageSpec<T>): JsonResult<T[]>;
|
|
423
473
|
/**
|
|
424
474
|
* 200 — `text/event-stream` of JSON `data:` frames, then `data: [DONE]`.
|
|
425
|
-
* Pass {@link Fx.stream} or any async iterable of
|
|
475
|
+
* Pass {@link Fx.stream} or any async iterable of chunks.
|
|
426
476
|
*/
|
|
427
|
-
stream(chunks: AsyncIterable<
|
|
477
|
+
stream(chunks: AsyncIterable<unknown>): JsonStreamResult;
|
|
428
478
|
}
|
|
429
479
|
|
|
430
480
|
/**
|
|
@@ -638,6 +688,29 @@ export interface Fx {
|
|
|
638
688
|
*/
|
|
639
689
|
deadLetters<T>(signal: SignalDecl<T>): Promise<readonly DeadLetter<T>[]>;
|
|
640
690
|
deadLetters(signal: NamedRef): Promise<readonly DeadLetter[]>;
|
|
691
|
+
/**
|
|
692
|
+
* Stream a `delivery: "live"` signal as SSE (records `read` on `signal:<name>`).
|
|
693
|
+
*
|
|
694
|
+
* HTTP `Last-Event-ID` is applied when `opts.afterId` is omitted. A missing
|
|
695
|
+
* cursor throws OKE1014; `JsonStreamResult.ready` turns that into HTTP 410.
|
|
696
|
+
*
|
|
697
|
+
* @param signal - Signal name or handle
|
|
698
|
+
* @param opts - Payload filter and optional resume cursor
|
|
699
|
+
*/
|
|
700
|
+
live<T>(
|
|
701
|
+
signal: SignalDecl<T>,
|
|
702
|
+
opts?: {
|
|
703
|
+
readonly match?: (payload: T) => boolean;
|
|
704
|
+
readonly afterId?: string;
|
|
705
|
+
},
|
|
706
|
+
): JsonStreamResult;
|
|
707
|
+
live(
|
|
708
|
+
signal: NamedRef,
|
|
709
|
+
opts?: {
|
|
710
|
+
readonly match?: (payload: unknown) => boolean;
|
|
711
|
+
readonly afterId?: string;
|
|
712
|
+
},
|
|
713
|
+
): JsonStreamResult;
|
|
641
714
|
/**
|
|
642
715
|
* Call another flow (records `call`). Stub returns `undefined`.
|
|
643
716
|
*
|
|
@@ -837,8 +910,27 @@ export interface CreateFxOptions {
|
|
|
837
910
|
readonly capability?: CapabilityToken;
|
|
838
911
|
/** Injectable clock for timestamps / `fx.clock.now`. */
|
|
839
912
|
readonly now?: () => number;
|
|
840
|
-
/** Auth principal. */
|
|
841
|
-
readonly auth?:
|
|
913
|
+
/** Auth principal bag (key methods attach at create time). */
|
|
914
|
+
readonly auth?: FxAuthIdentity;
|
|
915
|
+
/** Shared API key store for {@link Fx.auth} key methods. */
|
|
916
|
+
readonly apiKeyStore?: ApiKeyStore;
|
|
917
|
+
/** Tenant registry (when `gate.auth.tenant` is on). */
|
|
918
|
+
readonly tenantStore?: TenantStore;
|
|
919
|
+
/** Session store for {@link Fx.auth.switchTenant}. */
|
|
920
|
+
readonly sessions?: SessionStore;
|
|
921
|
+
/** Session crypto for {@link Fx.auth.switchTenant}. */
|
|
922
|
+
readonly sessionCrypto?: SessionCrypto;
|
|
923
|
+
/** Manifest for tenant-role catalog validation. */
|
|
924
|
+
readonly manifest?: Manifest | null;
|
|
925
|
+
/** When true, tenant-scoped KV / vault defaults apply. */
|
|
926
|
+
readonly tenantEnabled?: boolean;
|
|
927
|
+
/**
|
|
928
|
+
* When false, skip tenant-role scope union (tenant-unaware flow).
|
|
929
|
+
* Default true when {@link tenantEnabled}.
|
|
930
|
+
*/
|
|
931
|
+
readonly flowTenantScoped?: boolean;
|
|
932
|
+
/** Current flow plane (tenant-role union is user-plane only). */
|
|
933
|
+
readonly flowPlane?: "user" | "operator";
|
|
842
934
|
/** Operator principal. */
|
|
843
935
|
readonly operator?: FxOperator;
|
|
844
936
|
/**
|
|
@@ -911,6 +1003,8 @@ export interface CreateFxOptions {
|
|
|
911
1003
|
* as `parentRunId` so consuming Flows can join the trace chain.
|
|
912
1004
|
*/
|
|
913
1005
|
readonly runId?: string;
|
|
1006
|
+
/** HTTP `Last-Event-ID` for {@link Fx.live} resume (tests may pass `opts.afterId`). */
|
|
1007
|
+
readonly lastEventId?: string;
|
|
914
1008
|
/** Reveal PII through the store runtime (requires `pii:reveal` upstream). */
|
|
915
1009
|
readonly revealPii?: boolean;
|
|
916
1010
|
/** Trigger gate names for RLS (`oke.gate` = first policy/public). */
|
|
@@ -1001,12 +1095,20 @@ export function createFxContext(options: CreateFxOptions): FxContext {
|
|
|
1001
1095
|
}
|
|
1002
1096
|
const cacheStore = new Map<string, unknown>();
|
|
1003
1097
|
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1098
|
+
// Computed stem — a static import would pin HMAC / api-keys on every createFx.
|
|
1099
|
+
const authBag = options.auth ?? { userId: null, scopes: new Set() };
|
|
1100
|
+
const keys = lazyRequire<typeof import("./fx-auth-keys.ts")>(
|
|
1101
|
+
import.meta.dir,
|
|
1102
|
+
["fx", "auth", "keys"].join("-"),
|
|
1103
|
+
).attach({
|
|
1104
|
+
auth: authBag,
|
|
1105
|
+
store: options.apiKeyStore,
|
|
1106
|
+
now,
|
|
1107
|
+
gated,
|
|
1108
|
+
});
|
|
1109
|
+
const auth: FxAuth = keys as FxAuth;
|
|
1008
1110
|
const operator: FxOperator = options.operator ?? { id: null };
|
|
1009
|
-
const tenant:
|
|
1111
|
+
const tenant: { id: string | null } = options.tenant ?? { id: null };
|
|
1010
1112
|
const defaultLocale = options.i18n?.defaultLocale ?? "en";
|
|
1011
1113
|
const locale = options.i18n?.locale ?? defaultLocale;
|
|
1012
1114
|
const catalogs = options.i18n?.catalogs ?? {};
|
|
@@ -1042,6 +1144,7 @@ export function createFxContext(options: CreateFxOptions): FxContext {
|
|
|
1042
1144
|
gateNames: options.rlsGateNames ?? [],
|
|
1043
1145
|
bypass: false,
|
|
1044
1146
|
operator: false,
|
|
1147
|
+
...(options.tenantEnabled === true ? { tenantId: tenant.id } : {}),
|
|
1045
1148
|
});
|
|
1046
1149
|
return identity ? { rls: identity } : {};
|
|
1047
1150
|
}
|
|
@@ -1171,9 +1274,9 @@ export function createFxContext(options: CreateFxOptions): FxContext {
|
|
|
1171
1274
|
get driverId() {
|
|
1172
1275
|
return cached?.driverId ?? "memory";
|
|
1173
1276
|
},
|
|
1174
|
-
select(columns
|
|
1277
|
+
select: ((columns?: unknown) => {
|
|
1175
1278
|
return {
|
|
1176
|
-
from(table) {
|
|
1279
|
+
from(table: unknown) {
|
|
1177
1280
|
const run = (plan: {
|
|
1178
1281
|
where?: unknown;
|
|
1179
1282
|
orders?: readonly unknown[];
|
|
@@ -1182,7 +1285,7 @@ export function createFxContext(options: CreateFxOptions): FxContext {
|
|
|
1182
1285
|
}): Promise<SqlRow[]> =>
|
|
1183
1286
|
gatedTable("read", table, async () => {
|
|
1184
1287
|
const h = await ensure();
|
|
1185
|
-
const from = h.select(columns).from(table);
|
|
1288
|
+
const from = h.select(columns).from(table) as SelectFromBuilder;
|
|
1186
1289
|
const filtered = plan.where === undefined ? from : from.where(plan.where);
|
|
1187
1290
|
const ordered =
|
|
1188
1291
|
plan.orders === undefined ? filtered : filtered.orderBy(...plan.orders);
|
|
@@ -1206,7 +1309,7 @@ export function createFxContext(options: CreateFxOptions): FxContext {
|
|
|
1206
1309
|
});
|
|
1207
1310
|
|
|
1208
1311
|
return {
|
|
1209
|
-
where(where) {
|
|
1312
|
+
where(where: unknown) {
|
|
1210
1313
|
return {
|
|
1211
1314
|
...tail({ where }),
|
|
1212
1315
|
orderBy: (...orders: readonly unknown[]) => tail({ where, orders }),
|
|
@@ -1219,13 +1322,16 @@ export function createFxContext(options: CreateFxOptions): FxContext {
|
|
|
1219
1322
|
offset(n: number) {
|
|
1220
1323
|
return run({ offset: n });
|
|
1221
1324
|
},
|
|
1222
|
-
then(
|
|
1325
|
+
then(
|
|
1326
|
+
onfulfilled: (value: SqlRow[]) => unknown,
|
|
1327
|
+
onrejected?: (reason: unknown) => unknown,
|
|
1328
|
+
) {
|
|
1223
1329
|
return run({}).then(onfulfilled, onrejected);
|
|
1224
1330
|
},
|
|
1225
1331
|
};
|
|
1226
1332
|
},
|
|
1227
1333
|
};
|
|
1228
|
-
},
|
|
1334
|
+
}) as SqlStoreHandle["select"],
|
|
1229
1335
|
insert(table) {
|
|
1230
1336
|
return {
|
|
1231
1337
|
values(row) {
|
|
@@ -1338,6 +1444,33 @@ export function createFxContext(options: CreateFxOptions): FxContext {
|
|
|
1338
1444
|
} as SqlStoreHandle;
|
|
1339
1445
|
}
|
|
1340
1446
|
|
|
1447
|
+
function loadFxTenantStore(): {
|
|
1448
|
+
kv: (
|
|
1449
|
+
mode: "in" | "out",
|
|
1450
|
+
tenantId: string | null,
|
|
1451
|
+
decl: KvStoreDecl,
|
|
1452
|
+
enabled: boolean,
|
|
1453
|
+
prop: string | symbol,
|
|
1454
|
+
payload: unknown,
|
|
1455
|
+
) => unknown;
|
|
1456
|
+
path: (
|
|
1457
|
+
tenantId: string | null,
|
|
1458
|
+
enabled: boolean,
|
|
1459
|
+
contracts: { get(name: string): { perTenant?: boolean } | undefined } | undefined,
|
|
1460
|
+
name: string,
|
|
1461
|
+
) => string;
|
|
1462
|
+
missingVault: (storagePath: string) => Error;
|
|
1463
|
+
} {
|
|
1464
|
+
return lazyRequire(import.meta.dir, ["fx", "tenant", "store"].join("-"));
|
|
1465
|
+
}
|
|
1466
|
+
|
|
1467
|
+
const tenantOn = options.tenantEnabled === true;
|
|
1468
|
+
|
|
1469
|
+
function tenantVaultPath(name: string): string {
|
|
1470
|
+
if (!tenantOn) return name;
|
|
1471
|
+
return loadFxTenantStore().path(tenant.id, true, options.vaultRuntime?.contracts, name);
|
|
1472
|
+
}
|
|
1473
|
+
|
|
1341
1474
|
function storeHandle(ref: SqlStoreDecl): SqlStoreHandle;
|
|
1342
1475
|
function storeHandle(ref: KvStoreDecl): KvStoreFxHandle;
|
|
1343
1476
|
function storeHandle(ref: FilesStoreDecl): FilesStoreFxHandle;
|
|
@@ -1589,7 +1722,19 @@ export function createFxContext(options: CreateFxOptions): FxContext {
|
|
|
1589
1722
|
const vaultSurface: FxVault = {
|
|
1590
1723
|
get(secret) {
|
|
1591
1724
|
const name = resolveName(secret);
|
|
1592
|
-
return gatedSecret(name, () => {
|
|
1725
|
+
return gatedSecret(name, async () => {
|
|
1726
|
+
const path = tenantVaultPath(name);
|
|
1727
|
+
if (path !== name) {
|
|
1728
|
+
if (options.vaultAdapter) {
|
|
1729
|
+
const rec = await options.vaultAdapter.get(path);
|
|
1730
|
+
if (!rec) {
|
|
1731
|
+
throw loadFxTenantStore().missingVault(path);
|
|
1732
|
+
}
|
|
1733
|
+
return new Redacted(rec.value);
|
|
1734
|
+
}
|
|
1735
|
+
const value = secrets[path] ?? secrets[name] ?? `[secret:${path}]`;
|
|
1736
|
+
return new Redacted(value);
|
|
1737
|
+
}
|
|
1593
1738
|
const value = options.vaultRuntime
|
|
1594
1739
|
? options.vaultRuntime.read(name)
|
|
1595
1740
|
: (secrets[name] ?? `[secret:${name}]`);
|
|
@@ -1600,7 +1745,8 @@ export function createFxContext(options: CreateFxOptions): FxContext {
|
|
|
1600
1745
|
const name = resolveName(path);
|
|
1601
1746
|
return gatedSecret(name, async () => {
|
|
1602
1747
|
refuseDryRunVaultWrite("set", name);
|
|
1603
|
-
const
|
|
1748
|
+
const storage = tenantVaultPath(name);
|
|
1749
|
+
const written = await vaultAdapterFor("set").set(storage, value, {
|
|
1604
1750
|
...(setOptions?.ttlMs !== undefined ? { ttlMs: setOptions.ttlMs } : {}),
|
|
1605
1751
|
...(setOptions?.metadata !== undefined ? { metadata: setOptions.metadata } : {}),
|
|
1606
1752
|
actor: vaultActor,
|
|
@@ -1612,7 +1758,8 @@ export function createFxContext(options: CreateFxOptions): FxContext {
|
|
|
1612
1758
|
const name = resolveName(path);
|
|
1613
1759
|
return gatedSecret(name, async () => {
|
|
1614
1760
|
refuseDryRunVaultWrite("rotate", name);
|
|
1615
|
-
const
|
|
1761
|
+
const storage = tenantVaultPath(name);
|
|
1762
|
+
const written = await vaultAdapterFor("rotate").rotate(storage, value, {
|
|
1616
1763
|
actor: vaultActor,
|
|
1617
1764
|
});
|
|
1618
1765
|
return { path: written.path, version: written.version };
|
|
@@ -1622,7 +1769,7 @@ export function createFxContext(options: CreateFxOptions): FxContext {
|
|
|
1622
1769
|
const name = resolveName(path);
|
|
1623
1770
|
return gatedSecret(name, async () => {
|
|
1624
1771
|
refuseDryRunVaultWrite("delete", name);
|
|
1625
|
-
return vaultAdapterFor("delete").delete(name, { actor: vaultActor });
|
|
1772
|
+
return vaultAdapterFor("delete").delete(tenantVaultPath(name), { actor: vaultActor });
|
|
1626
1773
|
});
|
|
1627
1774
|
},
|
|
1628
1775
|
async list(prefix) {
|
|
@@ -1705,6 +1852,18 @@ export function createFxContext(options: CreateFxOptions): FxContext {
|
|
|
1705
1852
|
return options.signalRuntime.deadLetters(name);
|
|
1706
1853
|
});
|
|
1707
1854
|
},
|
|
1855
|
+
live(
|
|
1856
|
+
signal: NamedRef,
|
|
1857
|
+
opts?: { readonly match?: (payload: unknown) => boolean; readonly afterId?: string },
|
|
1858
|
+
) {
|
|
1859
|
+
return loadFxLiveStream().createLiveStream({
|
|
1860
|
+
name: resolveName(signal),
|
|
1861
|
+
afterId: opts?.afterId ?? options.lastEventId,
|
|
1862
|
+
match: opts?.match,
|
|
1863
|
+
gated,
|
|
1864
|
+
signalRuntime: options.signalRuntime,
|
|
1865
|
+
}) as JsonStreamResult;
|
|
1866
|
+
},
|
|
1708
1867
|
call(flow, input) {
|
|
1709
1868
|
const name = resolveName(flow);
|
|
1710
1869
|
return gated("call", name, async () => {
|
|
@@ -1985,5 +2144,24 @@ export function createFxContext(options: CreateFxOptions): FxContext {
|
|
|
1985
2144
|
},
|
|
1986
2145
|
};
|
|
1987
2146
|
|
|
2147
|
+
if (options.tenantStore !== undefined || options.tenantEnabled === true) {
|
|
2148
|
+
lazyRequire<{
|
|
2149
|
+
install: (
|
|
2150
|
+
bag: { auth: FxAuthIdentity; store: (ref: never) => object },
|
|
2151
|
+
ctx: {
|
|
2152
|
+
readonly tenantId: string | null;
|
|
2153
|
+
readonly options: typeof options;
|
|
2154
|
+
readonly now: () => number;
|
|
2155
|
+
readonly gated: typeof gated;
|
|
2156
|
+
},
|
|
2157
|
+
) => void;
|
|
2158
|
+
}>(import.meta.dir, ["fx", "tenant", "store"].join("-")).install(fx, {
|
|
2159
|
+
tenantId: tenant.id,
|
|
2160
|
+
options,
|
|
2161
|
+
now,
|
|
2162
|
+
gated,
|
|
2163
|
+
});
|
|
2164
|
+
}
|
|
2165
|
+
|
|
1988
2166
|
return { fx, ledger, capability };
|
|
1989
2167
|
}
|
package/src/kernel/hooks.ts
CHANGED
|
@@ -156,7 +156,7 @@ export type PipelineEncoder = (result: {
|
|
|
156
156
|
readonly failure?: FlowFailure | undefined;
|
|
157
157
|
readonly output?: unknown;
|
|
158
158
|
readonly error?: unknown;
|
|
159
|
-
}) => Response
|
|
159
|
+
}) => Response | Promise<Response>;
|
|
160
160
|
|
|
161
161
|
/**
|
|
162
162
|
* Run the seven-stage pipeline around `handler`.
|
|
@@ -234,7 +234,7 @@ export async function runPipeline(
|
|
|
234
234
|
// (short-circuit wins; encoder input mirrors the app layer exactly).
|
|
235
235
|
if (encode !== undefined && ctx.response === undefined) {
|
|
236
236
|
const failure = isFlowFailure(ctx.error) ? ctx.error : undefined;
|
|
237
|
-
ctx.response = encode({ failure, output: ctx.result, error: ctx.error });
|
|
237
|
+
ctx.response = await encode({ failure, output: ctx.result, error: ctx.error });
|
|
238
238
|
}
|
|
239
239
|
|
|
240
240
|
await runStage("onResponse", hooks, ctx, fx);
|
|
@@ -22,37 +22,28 @@ import {
|
|
|
22
22
|
* @param path - Base path (`/notes`)
|
|
23
23
|
* @param ops - The five FlowDefs (usually `resource.all()`)
|
|
24
24
|
* @param gates - Shared gate chain
|
|
25
|
-
* @param isLive - Live on GET list + get
|
|
26
25
|
*/
|
|
27
26
|
export function httpResource<P extends string>(
|
|
28
27
|
path: P,
|
|
29
28
|
ops: ResourceFlowBag,
|
|
30
29
|
gates: readonly GateRef[] = [],
|
|
31
|
-
isLive = false,
|
|
32
30
|
): ResourceMount {
|
|
33
31
|
const id = `${path}/:id` as `${P}/:id`;
|
|
34
|
-
const verb = <M extends HttpMethod>(
|
|
35
|
-
method
|
|
36
|
-
p: P | `${P}/:id`,
|
|
37
|
-
live: boolean,
|
|
38
|
-
): HttpTrigger<M> => createHttpTrigger(method, p, gates, live);
|
|
32
|
+
const verb = <M extends HttpMethod>(method: M, p: P | `${P}/:id`): HttpTrigger<M> =>
|
|
33
|
+
createHttpTrigger(method, p, gates);
|
|
39
34
|
const mount: ResourceMount = {
|
|
40
35
|
[resourceMountBrand]: true,
|
|
41
36
|
gates,
|
|
42
|
-
isLive,
|
|
43
37
|
mounts: [
|
|
44
|
-
{ trigger: verb("GET", path
|
|
45
|
-
{ trigger: verb("POST", path
|
|
46
|
-
{ trigger: verb("GET", id
|
|
47
|
-
{ trigger: verb("PATCH", id
|
|
48
|
-
{ trigger: verb("DELETE", id
|
|
38
|
+
{ trigger: verb("GET", path), flow: ops.list },
|
|
39
|
+
{ trigger: verb("POST", path), flow: ops.create },
|
|
40
|
+
{ trigger: verb("GET", id), flow: ops.get },
|
|
41
|
+
{ trigger: verb("PATCH", id), flow: ops.update },
|
|
42
|
+
{ trigger: verb("DELETE", id), flow: ops.remove },
|
|
49
43
|
],
|
|
50
|
-
gate: createGateAttach((next) => httpResource(path, ops, next
|
|
44
|
+
gate: createGateAttach((next) => httpResource(path, ops, next), gates),
|
|
51
45
|
public() {
|
|
52
|
-
return httpResource(path, ops, [...gates, GATE_PUBLIC_NAME]
|
|
53
|
-
},
|
|
54
|
-
live() {
|
|
55
|
-
return httpResource(path, ops, gates, true);
|
|
46
|
+
return httpResource(path, ops, [...gates, GATE_PUBLIC_NAME]);
|
|
56
47
|
},
|
|
57
48
|
};
|
|
58
49
|
return mount;
|
package/src/kernel/index.ts
CHANGED
|
@@ -78,6 +78,7 @@ export type {
|
|
|
78
78
|
AppFlowRoute,
|
|
79
79
|
AppRouteMap,
|
|
80
80
|
FlowNamespace,
|
|
81
|
+
LiveAppFlowRoute,
|
|
81
82
|
RouteFromFlow,
|
|
82
83
|
RoutesFromAdoptArgs,
|
|
83
84
|
} from "./adopt-routes.ts";
|
|
@@ -359,6 +360,7 @@ export {
|
|
|
359
360
|
type HttpMethod,
|
|
360
361
|
type HttpTrigger,
|
|
361
362
|
type InternalTrigger,
|
|
363
|
+
type LiveHttpTrigger,
|
|
362
364
|
type ResourceFlowBag,
|
|
363
365
|
type ResourceMount,
|
|
364
366
|
type SignalAsTrigger,
|
package/src/kernel/journal.ts
CHANGED
|
@@ -104,6 +104,8 @@ export interface JournalRun {
|
|
|
104
104
|
leaseExpiresAt?: number;
|
|
105
105
|
readonly createdAt: number;
|
|
106
106
|
updatedAt: number;
|
|
107
|
+
/** Isolation context for resume (`fx.tenant`). */
|
|
108
|
+
tenant?: string | null;
|
|
107
109
|
}
|
|
108
110
|
|
|
109
111
|
/**
|
|
@@ -366,6 +368,12 @@ export interface JournalSession {
|
|
|
366
368
|
readonly runId: string;
|
|
367
369
|
/** Underlying run snapshot (mutated as entries append). */
|
|
368
370
|
readonly run: JournalRun;
|
|
371
|
+
/**
|
|
372
|
+
* Persist isolation context so resume restamps {@link Fx.tenant}.
|
|
373
|
+
*
|
|
374
|
+
* @param id - Tenant id (null clears)
|
|
375
|
+
*/
|
|
376
|
+
stampTenant(id: string | null): Promise<void>;
|
|
369
377
|
/**
|
|
370
378
|
* Replay or execute a named step. Never re-runs `fn` when already journaled.
|
|
371
379
|
*
|
|
@@ -607,6 +615,10 @@ export function createJournal(options: CreateJournalOptions): Journal {
|
|
|
607
615
|
undoStack() {
|
|
608
616
|
return undos;
|
|
609
617
|
},
|
|
618
|
+
async stampTenant(id) {
|
|
619
|
+
run.tenant = id;
|
|
620
|
+
await persist();
|
|
621
|
+
},
|
|
610
622
|
beginRegistrationPass() {
|
|
611
623
|
registrationPass = true;
|
|
612
624
|
},
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Live HTTP exposure uniqueness (OKE1013) and GET-only synthesis.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, expect, test, beforeEach } from "bun:test";
|
|
6
|
+
import { z } from "zod";
|
|
7
|
+
import { gate } from "../elements/gate.ts";
|
|
8
|
+
import { signal, resetSignals } from "../elements/signal/declare.ts";
|
|
9
|
+
import { oke } from "./app.ts";
|
|
10
|
+
import { resetFlowSeq } from "./flow.ts";
|
|
11
|
+
import { on, resetBindings } from "./on.ts";
|
|
12
|
+
import { stampFlowName } from "./stamp-http.ts";
|
|
13
|
+
import { http } from "./triggers.ts";
|
|
14
|
+
|
|
15
|
+
const member = gate.policy("member", ({ auth }) => !!auth.verified);
|
|
16
|
+
const admin = gate.policy("admin", ({ auth }) => !!auth.verified);
|
|
17
|
+
const partner = gate.policy("partner", ({ auth }) => !!auth.verified);
|
|
18
|
+
|
|
19
|
+
const orderStatus = () =>
|
|
20
|
+
signal("order-status", {
|
|
21
|
+
delivery: "live",
|
|
22
|
+
optional: true,
|
|
23
|
+
schema: z.object({
|
|
24
|
+
orderId: z.string(),
|
|
25
|
+
status: z.enum(["placed", "fulfilling", "shipped"]),
|
|
26
|
+
}),
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
beforeEach(() => {
|
|
30
|
+
resetBindings();
|
|
31
|
+
resetFlowSeq();
|
|
32
|
+
resetSignals();
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
describe("live HTTP uniqueness", () => {
|
|
36
|
+
test("member :orderId + admin firehose both boot", () => {
|
|
37
|
+
const sig = orderStatus();
|
|
38
|
+
const events = on(http.get("/orders/:orderId/events").gate(member).live(sig));
|
|
39
|
+
const feed = on(http.get("/admin/order-status").gate(admin).live(sig));
|
|
40
|
+
stampFlowName(events, "orders.events");
|
|
41
|
+
stampFlowName(feed, "admin.adminFeed");
|
|
42
|
+
expect(() => oke({ name: "t", autoBoot: false })).not.toThrow();
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test("same match, different gates boot (via disambiguates on the client)", () => {
|
|
46
|
+
const sig = orderStatus();
|
|
47
|
+
const a = on(http.get("/orders/:orderId/events").gate(member).live(sig));
|
|
48
|
+
const b = on(http.get("/partners/:orderId/events").gate(partner).live(sig));
|
|
49
|
+
stampFlowName(a, "orders.events");
|
|
50
|
+
stampFlowName(b, "partners.events");
|
|
51
|
+
expect(() => oke({ name: "t", autoBoot: false })).not.toThrow();
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test("two member firehoses on different paths fail OKE1013", () => {
|
|
55
|
+
const sig = orderStatus();
|
|
56
|
+
const a = on(http.get("/feed-a").gate(member).live(sig));
|
|
57
|
+
const b = on(http.get("/feed-b").gate(member).live(sig));
|
|
58
|
+
stampFlowName(a, "orders.feedA");
|
|
59
|
+
stampFlowName(b, "orders.feedB");
|
|
60
|
+
expect(() => oke({ name: "t", autoBoot: false })).toThrow(/OKE1013/);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test("duplicate GET path still OKE1011", () => {
|
|
64
|
+
const sig = orderStatus();
|
|
65
|
+
const a = on(http.get("/orders/:orderId/events").gate(member).live(sig));
|
|
66
|
+
const b = on(http.get("/orders/:orderId/events").gate(admin).live(sig));
|
|
67
|
+
stampFlowName(a, "orders.events");
|
|
68
|
+
stampFlowName(b, "admin.events");
|
|
69
|
+
expect(() => oke({ name: "t", autoBoot: false })).toThrow(/OKE1011/);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test("POST .live(signal) is rejected", () => {
|
|
73
|
+
const sig = orderStatus();
|
|
74
|
+
expect(() => on(http.post("/orders").gate(member).live(sig))).toThrow(
|
|
75
|
+
/live exposure must be GET/,
|
|
76
|
+
);
|
|
77
|
+
});
|
|
78
|
+
});
|