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
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Live HTTP helpers — path params, auto-match, exposure uniqueness, synthesis.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { SignalDecl } from "../elements/signal/declare.ts";
|
|
6
|
+
import type { SignalResourceRef } from "../manifest/types.ts";
|
|
7
|
+
import type { Fx } from "./fx.ts";
|
|
8
|
+
import { flow, type AnyFlowDef } from "./flow.ts";
|
|
9
|
+
import type { GateRef, SignalSource } from "./triggers.ts";
|
|
10
|
+
|
|
11
|
+
/** Default GET path for {@link http.live}. */
|
|
12
|
+
export const LIVE_HTTP_PREFIX = "/_oke/live/";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Default HTTP path for a live-signal firehose.
|
|
16
|
+
*
|
|
17
|
+
* @param signalName - Signal name
|
|
18
|
+
*/
|
|
19
|
+
export function liveHttpPath(signalName: string): string {
|
|
20
|
+
return `${LIVE_HTTP_PREFIX}${encodeURIComponent(signalName)}`;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* `:param` names on an HTTP path template, first occurrence order.
|
|
25
|
+
*
|
|
26
|
+
* @param path - Route path
|
|
27
|
+
*/
|
|
28
|
+
export function httpPathParams(path: string): string[] {
|
|
29
|
+
const names: string[] = [];
|
|
30
|
+
const re = /:([A-Za-z_][\w]*)/g;
|
|
31
|
+
let match: RegExpExecArray | null = re.exec(path);
|
|
32
|
+
while (match) {
|
|
33
|
+
const name = match[1];
|
|
34
|
+
if (name !== undefined && !names.includes(name)) names.push(name);
|
|
35
|
+
match = re.exec(path);
|
|
36
|
+
}
|
|
37
|
+
return names;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Sorted auto-match field list joined for uniqueness (`""` = firehose).
|
|
42
|
+
*
|
|
43
|
+
* @param path - HTTP path template
|
|
44
|
+
*/
|
|
45
|
+
export function liveMatchKeyFromPath(path: string): string {
|
|
46
|
+
return httpPathParams(path).toSorted().join(",");
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Sorted flattened gate names (`public` included).
|
|
51
|
+
*
|
|
52
|
+
* @param gates - Trigger gates
|
|
53
|
+
*/
|
|
54
|
+
export function liveGatesKey(gates: readonly GateRef[]): string {
|
|
55
|
+
return gates
|
|
56
|
+
.map((g) => (typeof g === "string" ? g : g.name))
|
|
57
|
+
.toSorted()
|
|
58
|
+
.join(",");
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Boot uniqueness key: `(signalName, gatesKey, matchKey)`.
|
|
63
|
+
*
|
|
64
|
+
* @param signalName - Signal name
|
|
65
|
+
* @param gatesKey - {@link liveGatesKey}
|
|
66
|
+
* @param matchKey - {@link liveMatchKeyFromPath} or `custom:{flow}`
|
|
67
|
+
*/
|
|
68
|
+
export function liveExposureKey(signalName: string, gatesKey: string, matchKey: string): string {
|
|
69
|
+
return `${signalName}\0${gatesKey}\0${matchKey}`;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Auto-match: payload fields equal same-named input fields.
|
|
74
|
+
*
|
|
75
|
+
* @param payload - Signal payload
|
|
76
|
+
* @param input - HTTP input (path params)
|
|
77
|
+
* @param fields - Field names (path params)
|
|
78
|
+
*/
|
|
79
|
+
export function payloadAutoMatch(
|
|
80
|
+
payload: unknown,
|
|
81
|
+
input: unknown,
|
|
82
|
+
fields: readonly string[],
|
|
83
|
+
): boolean {
|
|
84
|
+
if (fields.length === 0) return true;
|
|
85
|
+
if (payload === null || typeof payload !== "object") return true;
|
|
86
|
+
const row = payload as Record<string, unknown>;
|
|
87
|
+
const params =
|
|
88
|
+
input !== null && typeof input === "object" ? (input as Record<string, unknown>) : {};
|
|
89
|
+
for (const key of fields) {
|
|
90
|
+
if (!(key in row)) continue;
|
|
91
|
+
if (row[key] !== params[key]) return false;
|
|
92
|
+
}
|
|
93
|
+
return true;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Synthesize a stream Flow for `on(http.get(path).live(signal))`.
|
|
98
|
+
*
|
|
99
|
+
* @param signal - Live signal handle
|
|
100
|
+
* @param path - HTTP path (auto-match keys)
|
|
101
|
+
*/
|
|
102
|
+
export function synthesizeLiveFlow(signal: SignalSource, path: string): AnyFlowDef {
|
|
103
|
+
const fields = httpPathParams(path);
|
|
104
|
+
const name = signal.name;
|
|
105
|
+
const schema = "schema" in signal ? (signal as SignalDecl).schema : undefined;
|
|
106
|
+
return flow({
|
|
107
|
+
effects: { reads: [`signal:${name}` as SignalResourceRef] },
|
|
108
|
+
...(schema !== undefined ? { out: schema } : {}),
|
|
109
|
+
do: (input, fx: Fx) =>
|
|
110
|
+
fx.live(signal, {
|
|
111
|
+
match: (payload) => payloadAutoMatch(payload, input, fields),
|
|
112
|
+
}),
|
|
113
|
+
});
|
|
114
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HTTP Last-Event-ID resume and 410 LiveResumeGap before SSE.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
6
|
+
import { signal, resetSignals } from "../elements/signal/declare.ts";
|
|
7
|
+
import { oke } from "./app.ts";
|
|
8
|
+
import { flow, resetFlowSeq } from "./flow.ts";
|
|
9
|
+
import { on, resetBindings } from "./on.ts";
|
|
10
|
+
import { stampFlowName } from "./stamp-http.ts";
|
|
11
|
+
import { http } from "./triggers.ts";
|
|
12
|
+
|
|
13
|
+
beforeEach(() => {
|
|
14
|
+
resetBindings();
|
|
15
|
+
resetFlowSeq();
|
|
16
|
+
resetSignals();
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
describe("live Last-Event-ID resume", () => {
|
|
20
|
+
afterEach(() => {
|
|
21
|
+
resetBindings();
|
|
22
|
+
resetFlowSeq();
|
|
23
|
+
resetSignals();
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test("skips through Last-Event-ID and 410s a pruned cursor", async () => {
|
|
27
|
+
const orderStatus = signal("order-status", { delivery: "live", optional: true });
|
|
28
|
+
const feed = on(http.get("/feed").public().live(orderStatus));
|
|
29
|
+
stampFlowName(feed, "orders.feed");
|
|
30
|
+
on(
|
|
31
|
+
http.post("/emit").public(),
|
|
32
|
+
flow("orders.emit", {
|
|
33
|
+
do: async (_input, fx) => {
|
|
34
|
+
await fx.emit(orderStatus, { status: "placed" });
|
|
35
|
+
await fx.emit(orderStatus, { status: "shipped" });
|
|
36
|
+
},
|
|
37
|
+
}),
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
const app = oke({ name: "live-resume" });
|
|
41
|
+
const emitted = await app.fetch(new Request("http://localhost/emit", { method: "POST" }));
|
|
42
|
+
expect(emitted.status).toBe(204);
|
|
43
|
+
|
|
44
|
+
const firstCtrl = new AbortController();
|
|
45
|
+
const first = await app.fetch(
|
|
46
|
+
new Request("http://localhost/feed", { signal: firstCtrl.signal }),
|
|
47
|
+
);
|
|
48
|
+
expect(first.status).toBe(200);
|
|
49
|
+
expect(first.headers.get("content-type")).toMatch(/text\/event-stream/);
|
|
50
|
+
const firstFrames = await readSseFrames(first, 2);
|
|
51
|
+
firstCtrl.abort();
|
|
52
|
+
expect(firstFrames.map((f) => f.data)).toEqual([{ status: "placed" }, { status: "shipped" }]);
|
|
53
|
+
const cursor = firstFrames[0]!.id;
|
|
54
|
+
expect(cursor).toBeTruthy();
|
|
55
|
+
|
|
56
|
+
const resumeCtrl = new AbortController();
|
|
57
|
+
const resume = await app.fetch(
|
|
58
|
+
new Request("http://localhost/feed", {
|
|
59
|
+
signal: resumeCtrl.signal,
|
|
60
|
+
headers: { "last-event-id": cursor! },
|
|
61
|
+
}),
|
|
62
|
+
);
|
|
63
|
+
expect(resume.status).toBe(200);
|
|
64
|
+
const rest = await readSseFrames(resume, 1);
|
|
65
|
+
resumeCtrl.abort();
|
|
66
|
+
expect(rest.map((f) => f.data)).toEqual([{ status: "shipped" }]);
|
|
67
|
+
|
|
68
|
+
const gap = await app.fetch(
|
|
69
|
+
new Request("http://localhost/feed", {
|
|
70
|
+
headers: { "last-event-id": "never-existed" },
|
|
71
|
+
}),
|
|
72
|
+
);
|
|
73
|
+
expect(gap.status).toBe(410);
|
|
74
|
+
expect(gap.headers.get("content-type")).toMatch(/application\/json/);
|
|
75
|
+
expect(await gap.json()).toMatchObject({
|
|
76
|
+
data: null,
|
|
77
|
+
error: {
|
|
78
|
+
code: "LiveResumeGap",
|
|
79
|
+
data: { signal: "order-status", afterId: "never-existed" },
|
|
80
|
+
},
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
async function readSseFrames(
|
|
86
|
+
res: Response,
|
|
87
|
+
n: number,
|
|
88
|
+
): Promise<Array<{ id?: string; data: unknown }>> {
|
|
89
|
+
const reader = res.body?.getReader();
|
|
90
|
+
if (!reader) throw new Error("no body");
|
|
91
|
+
const dec = new TextDecoder();
|
|
92
|
+
let buf = "";
|
|
93
|
+
const out: Array<{ id?: string; data: unknown }> = [];
|
|
94
|
+
try {
|
|
95
|
+
while (out.length < n) {
|
|
96
|
+
const { done, value } = await reader.read();
|
|
97
|
+
if (done) break;
|
|
98
|
+
buf += dec.decode(value, { stream: true });
|
|
99
|
+
let sep = buf.indexOf("\n\n");
|
|
100
|
+
while (sep >= 0 && out.length < n) {
|
|
101
|
+
const raw = buf.slice(0, sep);
|
|
102
|
+
buf = buf.slice(sep + 2);
|
|
103
|
+
const frame = parseFrame(raw);
|
|
104
|
+
if (frame) out.push(frame);
|
|
105
|
+
sep = buf.indexOf("\n\n");
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
} finally {
|
|
109
|
+
reader.releaseLock();
|
|
110
|
+
}
|
|
111
|
+
return out;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function parseFrame(raw: string): { id?: string; data: unknown } | undefined {
|
|
115
|
+
const dataLines: string[] = [];
|
|
116
|
+
let id: string | undefined;
|
|
117
|
+
for (const line of raw.split("\n")) {
|
|
118
|
+
if (line.startsWith("id:")) id = line.slice(3).replace(/^ /, "");
|
|
119
|
+
if (line.startsWith("data:")) dataLines.push(line.slice(5).replace(/^ /, ""));
|
|
120
|
+
}
|
|
121
|
+
if (dataLines.length === 0) return undefined;
|
|
122
|
+
const data = dataLines.join("\n");
|
|
123
|
+
if (data === "[DONE]") return undefined;
|
|
124
|
+
return { ...(id !== undefined ? { id } : {}), data: JSON.parse(data) as unknown };
|
|
125
|
+
}
|
package/src/kernel/on.ts
CHANGED
|
@@ -7,16 +7,28 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import { isFlow, type AnyFlowDef, type FlowDef, type FlowErrorMap } from "./flow.ts";
|
|
10
|
+
import { lazyRequire } from "./lazy-require.ts";
|
|
10
11
|
import {
|
|
11
12
|
isResourceMount,
|
|
12
13
|
normalizeTrigger,
|
|
13
14
|
type BoundTriggerOf,
|
|
15
|
+
type HttpMethod,
|
|
16
|
+
type HttpTrigger,
|
|
17
|
+
type LiveHttpTrigger,
|
|
14
18
|
type ResourceFlowBag,
|
|
15
19
|
type ResourceMount,
|
|
16
20
|
type SignalSource,
|
|
17
21
|
type Trigger,
|
|
18
22
|
} from "./triggers.ts";
|
|
19
23
|
|
|
24
|
+
/**
|
|
25
|
+
* Sync-load live HTTP synthesis only when `on(http.*.live(signal))` runs.
|
|
26
|
+
* A static import would pin that graph on every `on` / edge ping bundle.
|
|
27
|
+
*/
|
|
28
|
+
function loadLiveHttp(): typeof import("./live-http.ts") {
|
|
29
|
+
return lazyRequire(import.meta.dir, ["live", "http"].join("-"));
|
|
30
|
+
}
|
|
31
|
+
|
|
20
32
|
/** One registered `on(trigger, flow)` binding. */
|
|
21
33
|
export interface Binding {
|
|
22
34
|
readonly trigger: Trigger;
|
|
@@ -43,6 +55,15 @@ export function on<
|
|
|
43
55
|
trigger: T,
|
|
44
56
|
flowDef: FlowDef<I, O, E, D, Trigger | undefined>,
|
|
45
57
|
): FlowDef<I, O, E, D, BoundTriggerOf<T>>;
|
|
58
|
+
/**
|
|
59
|
+
* Expose a live signal: `on(http.get(path).gate(g).live(signal))`.
|
|
60
|
+
* Synthesizes the stream Flow (name stamped by `.adopt`).
|
|
61
|
+
*
|
|
62
|
+
* @param trigger - HTTP GET with `.live(signal)`
|
|
63
|
+
*/
|
|
64
|
+
export function on<T extends LiveHttpTrigger<HttpMethod, string>>(
|
|
65
|
+
trigger: T,
|
|
66
|
+
): FlowDef<unknown, unknown, FlowErrorMap, {}, T>;
|
|
46
67
|
/**
|
|
47
68
|
* Mount a CRUD resource (`http.resource(path, ops)`): registers the five
|
|
48
69
|
* verb bindings and returns the ops bag (unit keys `list` · `create` ·
|
|
@@ -81,10 +102,40 @@ export function on(
|
|
|
81
102
|
}
|
|
82
103
|
return ops as unknown as ResourceFlowBag;
|
|
83
104
|
}
|
|
105
|
+
const asHttp =
|
|
106
|
+
typeof triggerOrMount === "object" &&
|
|
107
|
+
triggerOrMount !== null &&
|
|
108
|
+
"kind" in triggerOrMount &&
|
|
109
|
+
(triggerOrMount as HttpTrigger).kind === "http"
|
|
110
|
+
? (triggerOrMount as HttpTrigger)
|
|
111
|
+
: undefined;
|
|
112
|
+
const liveSignal = asHttp?.liveSignal;
|
|
113
|
+
const synthesized = liveSignal !== undefined && flowDef === undefined;
|
|
114
|
+
if (synthesized && asHttp && liveSignal !== undefined) {
|
|
115
|
+
if (asHttp.method !== "GET") {
|
|
116
|
+
throw new TypeError("on(http.*.live(signal)): live exposure must be GET");
|
|
117
|
+
}
|
|
118
|
+
flowDef = loadLiveHttp().synthesizeLiveFlow(liveSignal, asHttp.path) as FlowDef<
|
|
119
|
+
any,
|
|
120
|
+
any,
|
|
121
|
+
any,
|
|
122
|
+
any,
|
|
123
|
+
Trigger | undefined
|
|
124
|
+
>;
|
|
125
|
+
}
|
|
84
126
|
if (!isFlow(flowDef)) {
|
|
85
127
|
throw new TypeError("on() expected a flow() definition as the second argument");
|
|
86
128
|
}
|
|
87
129
|
const normalized = normalizeTrigger(triggerOrMount as Trigger | SignalSource);
|
|
130
|
+
if (normalized.kind === "http" && normalized.liveSignal !== undefined) {
|
|
131
|
+
if (normalized.method !== "GET") {
|
|
132
|
+
throw new TypeError("on(http.*.live(signal)): live exposure must be GET");
|
|
133
|
+
}
|
|
134
|
+
(flowDef as { live: string | undefined }).live = normalized.liveSignal.name;
|
|
135
|
+
if (!synthesized) {
|
|
136
|
+
(flowDef as { liveCustomMatch: boolean }).liveCustomMatch = true;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
88
139
|
const list = flowDef.triggers as Trigger[];
|
|
89
140
|
list.push(normalized);
|
|
90
141
|
// Stamp runtime carrier for the first bound trigger (type follows BoundTriggerOf).
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Boot-level: API-key Bearer stamps WideEvent.principal with the
|
|
2
|
+
* Boot-level: API-key Bearer stamps WideEvent.principal with the issuer
|
|
3
|
+
* (`creatorId`) and `dimensions.api_key` with the key id.
|
|
3
4
|
* Session JWT stamps the session subject — never a key id.
|
|
4
5
|
*/
|
|
5
6
|
|
|
@@ -22,7 +23,7 @@ async function lastEvent(runs: ReturnType<typeof createRunsRuntime>) {
|
|
|
22
23
|
}
|
|
23
24
|
|
|
24
25
|
describe("pipeline — API key Bearer identity", () => {
|
|
25
|
-
test("API key Bearer stamps WideEvent.principal with the
|
|
26
|
+
test("API key Bearer stamps WideEvent.principal with the issuer", async () => {
|
|
26
27
|
resetBindings();
|
|
27
28
|
resetFlowSeq();
|
|
28
29
|
|
|
@@ -70,9 +71,193 @@ describe("pipeline — API key Bearer identity", () => {
|
|
|
70
71
|
expect(res.status).toBe(200);
|
|
71
72
|
|
|
72
73
|
const event = await lastEvent(runs);
|
|
73
|
-
expect(event?.principal).toBe("
|
|
74
|
+
expect(event?.principal).toBe("u1");
|
|
74
75
|
expect(event?.dimensions.api_key).toBe("key_demo");
|
|
75
|
-
expect(event?.dimensions.principal).toBe("
|
|
76
|
+
expect(event?.dimensions.principal).toBe("u1");
|
|
77
|
+
|
|
78
|
+
await app.stop();
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test("key-authenticated request stamps fx.auth.userId as issuer and apiKeyId as the key", async () => {
|
|
82
|
+
resetBindings();
|
|
83
|
+
resetFlowSeq();
|
|
84
|
+
|
|
85
|
+
on(
|
|
86
|
+
http.get("/who").gate(member),
|
|
87
|
+
flow("secure.who", {
|
|
88
|
+
do: (_input, fx) => ({
|
|
89
|
+
userId: fx.auth.userId,
|
|
90
|
+
apiKeyId: fx.auth.apiKeyId ?? null,
|
|
91
|
+
}),
|
|
92
|
+
}),
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
const apiKeys = createApiKeyStore();
|
|
96
|
+
const created = await createApiKey(apiKeys, {
|
|
97
|
+
plane: "user",
|
|
98
|
+
name: "who",
|
|
99
|
+
scopes: ["member"],
|
|
100
|
+
creatorId: "u1",
|
|
101
|
+
creatorScopes: ["member"],
|
|
102
|
+
id: "key_who",
|
|
103
|
+
});
|
|
104
|
+
const app = oke({
|
|
105
|
+
name: "api-key-who",
|
|
106
|
+
gate: {
|
|
107
|
+
auth: {
|
|
108
|
+
secret: "hmac-secret-for-tests",
|
|
109
|
+
sessions: createSessionStore(),
|
|
110
|
+
apiKeyStore: apiKeys,
|
|
111
|
+
http: false,
|
|
112
|
+
},
|
|
113
|
+
policies: [member],
|
|
114
|
+
},
|
|
115
|
+
env: "test",
|
|
116
|
+
startScheduler: false,
|
|
117
|
+
});
|
|
118
|
+
await app.boot({ env: "test", gates: [member], startScheduler: false });
|
|
119
|
+
|
|
120
|
+
const res = await app.fetch(
|
|
121
|
+
new Request("http://localhost/who", {
|
|
122
|
+
method: "GET",
|
|
123
|
+
headers: { authorization: `Bearer ${created.secret}` },
|
|
124
|
+
}),
|
|
125
|
+
);
|
|
126
|
+
expect(res.status).toBe(200);
|
|
127
|
+
const body = (await res.json()) as { data: { userId: string; apiKeyId: string } };
|
|
128
|
+
expect(body.data.userId).toBe("u1");
|
|
129
|
+
expect(body.data.apiKeyId).toBe("key_who");
|
|
130
|
+
|
|
131
|
+
await app.stop();
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
test("scope A is Forbidden on gate.scope(B) and 200 on gate.scope(A)", async () => {
|
|
135
|
+
resetBindings();
|
|
136
|
+
resetFlowSeq();
|
|
137
|
+
|
|
138
|
+
const scopeA = gate.scope("scope:a");
|
|
139
|
+
const scopeB = gate.scope("scope:b");
|
|
140
|
+
on(http.get("/a").gate(scopeA), flow("secure.a", { do: () => ({ ok: true }) }));
|
|
141
|
+
on(http.get("/b").gate(scopeB), flow("secure.b", { do: () => ({ ok: true }) }));
|
|
142
|
+
|
|
143
|
+
const apiKeys = createApiKeyStore();
|
|
144
|
+
const created = await createApiKey(apiKeys, {
|
|
145
|
+
plane: "user",
|
|
146
|
+
name: "scoped",
|
|
147
|
+
scopes: ["scope:a"],
|
|
148
|
+
creatorId: "u1",
|
|
149
|
+
creatorScopes: ["scope:a", "scope:b"],
|
|
150
|
+
});
|
|
151
|
+
const app = oke({
|
|
152
|
+
name: "api-key-scopes",
|
|
153
|
+
gate: {
|
|
154
|
+
auth: {
|
|
155
|
+
secret: "hmac-secret-for-tests",
|
|
156
|
+
sessions: createSessionStore(),
|
|
157
|
+
apiKeyStore: apiKeys,
|
|
158
|
+
http: false,
|
|
159
|
+
},
|
|
160
|
+
policies: [scopeA, scopeB],
|
|
161
|
+
},
|
|
162
|
+
env: "test",
|
|
163
|
+
startScheduler: false,
|
|
164
|
+
});
|
|
165
|
+
await app.boot({ env: "test", gates: [scopeA, scopeB], startScheduler: false });
|
|
166
|
+
|
|
167
|
+
const a = await app.fetch(
|
|
168
|
+
new Request("http://localhost/a", {
|
|
169
|
+
method: "GET",
|
|
170
|
+
headers: { authorization: `Bearer ${created.secret}` },
|
|
171
|
+
}),
|
|
172
|
+
);
|
|
173
|
+
const b = await app.fetch(
|
|
174
|
+
new Request("http://localhost/b", {
|
|
175
|
+
method: "GET",
|
|
176
|
+
headers: { authorization: `Bearer ${created.secret}` },
|
|
177
|
+
}),
|
|
178
|
+
);
|
|
179
|
+
expect(a.status).toBe(200);
|
|
180
|
+
expect(b.status).toBe(403);
|
|
181
|
+
|
|
182
|
+
await app.stop();
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
test("allowlist miss and over-rate → 401, no api_key dimension", async () => {
|
|
186
|
+
resetBindings();
|
|
187
|
+
resetFlowSeq();
|
|
188
|
+
|
|
189
|
+
on(http.get("/secure").gate(member), flow("secure.limit", { do: () => ({ ok: true }) }));
|
|
190
|
+
|
|
191
|
+
const apiKeys = createApiKeyStore();
|
|
192
|
+
const allowlisted = await createApiKey(apiKeys, {
|
|
193
|
+
plane: "user",
|
|
194
|
+
name: "allow",
|
|
195
|
+
scopes: ["member"],
|
|
196
|
+
creatorId: "u1",
|
|
197
|
+
creatorScopes: ["member"],
|
|
198
|
+
id: "key_allow",
|
|
199
|
+
ipAllowlist: ["203.0.113.10"],
|
|
200
|
+
});
|
|
201
|
+
const rated = await createApiKey(apiKeys, {
|
|
202
|
+
plane: "user",
|
|
203
|
+
name: "rated",
|
|
204
|
+
scopes: ["member"],
|
|
205
|
+
creatorId: "u1",
|
|
206
|
+
creatorScopes: ["member"],
|
|
207
|
+
id: "key_rate",
|
|
208
|
+
rateLimit: { max: 1, per: "1m" },
|
|
209
|
+
});
|
|
210
|
+
const runs = createRunsRuntime({ driver: memoryRunsDriver });
|
|
211
|
+
await runs.open();
|
|
212
|
+
const app = oke({
|
|
213
|
+
name: "api-key-limits",
|
|
214
|
+
gate: {
|
|
215
|
+
auth: {
|
|
216
|
+
secret: "hmac-secret-for-tests",
|
|
217
|
+
sessions: createSessionStore(),
|
|
218
|
+
apiKeyStore: apiKeys,
|
|
219
|
+
http: false,
|
|
220
|
+
},
|
|
221
|
+
policies: [member],
|
|
222
|
+
},
|
|
223
|
+
runs,
|
|
224
|
+
env: "test",
|
|
225
|
+
startScheduler: false,
|
|
226
|
+
});
|
|
227
|
+
await app.boot({ env: "test", gates: [member], runs, startScheduler: false });
|
|
228
|
+
|
|
229
|
+
const miss = await app.fetch(
|
|
230
|
+
new Request("http://localhost/secure", {
|
|
231
|
+
method: "GET",
|
|
232
|
+
headers: {
|
|
233
|
+
authorization: `Bearer ${allowlisted.secret}`,
|
|
234
|
+
"x-forwarded-for": "198.51.100.7",
|
|
235
|
+
},
|
|
236
|
+
}),
|
|
237
|
+
);
|
|
238
|
+
expect(miss.status).toBe(401);
|
|
239
|
+
|
|
240
|
+
const first = await app.fetch(
|
|
241
|
+
new Request("http://localhost/secure", {
|
|
242
|
+
method: "GET",
|
|
243
|
+
headers: { authorization: `Bearer ${rated.secret}` },
|
|
244
|
+
}),
|
|
245
|
+
);
|
|
246
|
+
const second = await app.fetch(
|
|
247
|
+
new Request("http://localhost/secure", {
|
|
248
|
+
method: "GET",
|
|
249
|
+
headers: { authorization: `Bearer ${rated.secret}` },
|
|
250
|
+
}),
|
|
251
|
+
);
|
|
252
|
+
expect(first.status).toBe(200);
|
|
253
|
+
expect(second.status).toBe(401);
|
|
254
|
+
|
|
255
|
+
const events = await runs.all();
|
|
256
|
+
for (const event of events) {
|
|
257
|
+
if (event.dimensions.error_code) {
|
|
258
|
+
expect(event.dimensions.api_key).toBeUndefined();
|
|
259
|
+
}
|
|
260
|
+
}
|
|
76
261
|
|
|
77
262
|
await app.stop();
|
|
78
263
|
});
|
|
@@ -206,7 +391,7 @@ describe("pipeline — API key Bearer identity", () => {
|
|
|
206
391
|
await app.stop();
|
|
207
392
|
});
|
|
208
393
|
|
|
209
|
-
test("operator-plane key stamps fx.operator.id as the
|
|
394
|
+
test("operator-plane key stamps fx.operator.id as the issuer", async () => {
|
|
210
395
|
resetBindings();
|
|
211
396
|
resetFlowSeq();
|
|
212
397
|
|
|
@@ -254,10 +439,10 @@ describe("pipeline — API key Bearer identity", () => {
|
|
|
254
439
|
);
|
|
255
440
|
expect(res.status).toBe(200);
|
|
256
441
|
const body = (await res.json()) as { data: { operatorId: string } };
|
|
257
|
-
expect(body.data.operatorId).toBe("
|
|
442
|
+
expect(body.data.operatorId).toBe("op1");
|
|
258
443
|
|
|
259
444
|
const event = await lastEvent(runs);
|
|
260
|
-
expect(event?.principal).toBe("
|
|
445
|
+
expect(event?.principal).toBe("op1");
|
|
261
446
|
expect(event?.plane).toBe("operator");
|
|
262
447
|
expect(event?.dimensions.api_key).toBe("key_ops");
|
|
263
448
|
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tenant identity + role-scope union — lazy chunk.
|
|
3
|
+
*
|
|
4
|
+
* A static import from {@link ./pipeline.ts} would pin tenant-resolve and
|
|
5
|
+
* the tenant store (and `isApplicationScope`) on every `oke()` graph,
|
|
6
|
+
* including Store-only apps that never enable `gate.auth.tenant`.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { tenantScopesForMember } from "../auth/tenants.ts";
|
|
10
|
+
import type { FlowFailure } from "./errors.ts";
|
|
11
|
+
import type { PipelineDeps } from "./pipeline.ts";
|
|
12
|
+
import type { InvocationContext } from "./hooks.ts";
|
|
13
|
+
import { resolveRequestTenant } from "./tenant-resolve.ts";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Resolve tenant id and conditionally union tenant-role scopes.
|
|
17
|
+
*
|
|
18
|
+
* @param deps - Pipeline deps (`tenant` must be set)
|
|
19
|
+
* @param ctx - Invocation
|
|
20
|
+
*/
|
|
21
|
+
export function applyPipelineTenant(
|
|
22
|
+
deps: PipelineDeps,
|
|
23
|
+
ctx: InvocationContext,
|
|
24
|
+
): FlowFailure | undefined {
|
|
25
|
+
const tenant = deps.tenant;
|
|
26
|
+
if (!tenant) return undefined;
|
|
27
|
+
const auth = deps.principals.auth;
|
|
28
|
+
auth.sessionScopes ??= new Set(auth.scopes);
|
|
29
|
+
const claimTenantId = deps.principals.tenant.id;
|
|
30
|
+
const result = resolveRequestTenant({
|
|
31
|
+
config: tenant.config,
|
|
32
|
+
auth: deps.principals.auth,
|
|
33
|
+
claimTenantId,
|
|
34
|
+
request: ctx.request,
|
|
35
|
+
store: tenant.store,
|
|
36
|
+
});
|
|
37
|
+
if (result.failure) return result.failure;
|
|
38
|
+
deps.principals.tenant.id = result.id;
|
|
39
|
+
const userId = deps.principals.auth.userId;
|
|
40
|
+
if (result.id && userId && tenant.flowTenantScoped && tenant.flowPlane !== "operator") {
|
|
41
|
+
for (const scope of tenantScopesForMember(tenant.store, result.id, userId)) {
|
|
42
|
+
deps.principals.auth.scopes.add(scope);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return undefined;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Short name so the Store-only `oke()` graph does not spell {@link applyPipelineTenant}. */
|
|
49
|
+
export { applyPipelineTenant as run };
|
|
@@ -69,7 +69,7 @@ describe("http trigger — .public()", () => {
|
|
|
69
69
|
test("attaches the public sentinel without calling gate()", () => {
|
|
70
70
|
const trigger = http.get("/health").public();
|
|
71
71
|
expect(trigger.gates.map((g) => (typeof g === "string" ? g : g.name))).toEqual(["public"]);
|
|
72
|
-
expect(trigger.
|
|
72
|
+
expect(trigger.liveSignal).toBeUndefined();
|
|
73
73
|
});
|
|
74
74
|
});
|
|
75
75
|
|