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,213 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tenant KV prefix + vault request-time path — lazy chunk.
|
|
3
|
+
*
|
|
4
|
+
* Inlined wrappers would sit on every `createFx` / Store-only `oke()` graph
|
|
5
|
+
* even when `gate.auth.tenant` is off.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { KvStoreDecl } from "../elements/store.ts";
|
|
9
|
+
import type { VaultRuntime } from "../elements/vault.ts";
|
|
10
|
+
import type { Manifest } from "../manifest/types.ts";
|
|
11
|
+
import type { SessionCrypto, SessionStore } from "../auth/sessions.ts";
|
|
12
|
+
import type { TenantStore } from "../auth/tenants.ts";
|
|
13
|
+
import { throwOke } from "./errors.ts";
|
|
14
|
+
import type { FxAuthIdentity } from "./fx-auth-keys.ts";
|
|
15
|
+
import { bind, type AttachAuthTenantMethodsOptions } from "./fx-auth-tenants.ts";
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Whether this KV namespace is tenant-prefixed.
|
|
19
|
+
*
|
|
20
|
+
* @param decl - KV store decl
|
|
21
|
+
* @param tenantEnabled - `gate.auth.tenant` is on
|
|
22
|
+
*/
|
|
23
|
+
export function kvTenantScoped(decl: KvStoreDecl, tenantEnabled: boolean): boolean {
|
|
24
|
+
if (decl.tenantScoped === false) return false;
|
|
25
|
+
if (decl.tenantScoped === true) return true;
|
|
26
|
+
return tenantEnabled;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Prefix KV args with `{tenantId}:`. Fails loud when no tenant is resolved.
|
|
31
|
+
*
|
|
32
|
+
* @param tenantId - Live `fx.tenant.id`
|
|
33
|
+
* @param decl - KV store decl
|
|
34
|
+
* @param tenantEnabled - Tenancy switch
|
|
35
|
+
* @param prop - Handle method
|
|
36
|
+
* @param args - Original args
|
|
37
|
+
*/
|
|
38
|
+
export function rewriteKvArgs(
|
|
39
|
+
tenantId: string | null,
|
|
40
|
+
decl: KvStoreDecl,
|
|
41
|
+
tenantEnabled: boolean,
|
|
42
|
+
prop: string | symbol,
|
|
43
|
+
args: unknown[],
|
|
44
|
+
): unknown[] {
|
|
45
|
+
if (!kvTenantScoped(decl, tenantEnabled)) return args;
|
|
46
|
+
if (!tenantId) throwOke("TENANT_REQUIRED");
|
|
47
|
+
const prefix = `${tenantId}:`;
|
|
48
|
+
if (prop === "list") {
|
|
49
|
+
const userPrefix = typeof args[0] === "string" ? args[0] : "";
|
|
50
|
+
return [`${prefix}${userPrefix}`];
|
|
51
|
+
}
|
|
52
|
+
if (typeof args[0] !== "string") return args;
|
|
53
|
+
return [`${prefix}${args[0]}`, ...args.slice(1)];
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Strip the tenant prefix from `list()` results.
|
|
58
|
+
*
|
|
59
|
+
* @param tenantId - Live `fx.tenant.id`
|
|
60
|
+
* @param decl - KV store decl
|
|
61
|
+
* @param tenantEnabled - Tenancy switch
|
|
62
|
+
* @param keys - Physical keys
|
|
63
|
+
*/
|
|
64
|
+
export function stripKvPrefix(
|
|
65
|
+
tenantId: string | null,
|
|
66
|
+
decl: KvStoreDecl,
|
|
67
|
+
tenantEnabled: boolean,
|
|
68
|
+
keys: string[],
|
|
69
|
+
): string[] {
|
|
70
|
+
if (!kvTenantScoped(decl, tenantEnabled) || !tenantId) return keys;
|
|
71
|
+
const prefix = `${tenantId}:`;
|
|
72
|
+
return keys.map((k) => (k.startsWith(prefix) ? k.slice(prefix.length) : k));
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Request-time vault path `{tenantId}/{contract}` when the contract is per-tenant.
|
|
77
|
+
*
|
|
78
|
+
* @param tenantId - Live `fx.tenant.id`
|
|
79
|
+
* @param tenantEnabled - Tenancy switch
|
|
80
|
+
* @param contracts - Vault runtime contracts
|
|
81
|
+
* @param contractName - Capability name
|
|
82
|
+
*/
|
|
83
|
+
export function vaultStoragePath(
|
|
84
|
+
tenantId: string | null,
|
|
85
|
+
tenantEnabled: boolean,
|
|
86
|
+
contracts: VaultRuntime["contracts"] | undefined,
|
|
87
|
+
contractName: string,
|
|
88
|
+
): string {
|
|
89
|
+
const decl = contracts?.get(contractName);
|
|
90
|
+
const perTenant =
|
|
91
|
+
decl?.perTenant === true || (tenantEnabled && decl !== undefined && decl.perTenant !== false);
|
|
92
|
+
if (!perTenant) return contractName;
|
|
93
|
+
if (!tenantId) throwOke("TENANT_REQUIRED");
|
|
94
|
+
return `${tenantId}/${contractName}`;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Compact KV rewrite used by `createFx` so long helper names stay off the
|
|
99
|
+
* Store-only `oke()` graph.
|
|
100
|
+
*
|
|
101
|
+
* @param mode - `in` prefixes args; `out` strips `list()` keys
|
|
102
|
+
* @param tenantId - Live `fx.tenant.id`
|
|
103
|
+
* @param decl - KV store decl
|
|
104
|
+
* @param tenantEnabled - Tenancy switch
|
|
105
|
+
* @param prop - Handle method
|
|
106
|
+
* @param payload - Args or list keys
|
|
107
|
+
*/
|
|
108
|
+
export function kv(
|
|
109
|
+
mode: "in" | "out",
|
|
110
|
+
tenantId: string | null,
|
|
111
|
+
decl: KvStoreDecl,
|
|
112
|
+
tenantEnabled: boolean,
|
|
113
|
+
prop: string | symbol,
|
|
114
|
+
payload: unknown,
|
|
115
|
+
): unknown {
|
|
116
|
+
if (mode === "out") return stripKvPrefix(tenantId, decl, tenantEnabled, payload as string[]);
|
|
117
|
+
return rewriteKvArgs(tenantId, decl, tenantEnabled, prop, payload as unknown[]);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Compact vault path used by `createFx`.
|
|
122
|
+
*
|
|
123
|
+
* @param tenantId - Live `fx.tenant.id`
|
|
124
|
+
* @param tenantEnabled - Tenancy switch
|
|
125
|
+
* @param contracts - Vault runtime contracts
|
|
126
|
+
* @param contractName - Capability name
|
|
127
|
+
*/
|
|
128
|
+
export function path(
|
|
129
|
+
tenantId: string | null,
|
|
130
|
+
tenantEnabled: boolean,
|
|
131
|
+
contracts: VaultRuntime["contracts"] | undefined,
|
|
132
|
+
contractName: string,
|
|
133
|
+
): string {
|
|
134
|
+
return vaultStoragePath(tenantId, tenantEnabled, contracts, contractName);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Fail loud when a per-tenant vault row is missing.
|
|
139
|
+
*
|
|
140
|
+
* @param storagePath - Physical `{tenantId}/{contract}` path
|
|
141
|
+
*/
|
|
142
|
+
export function missingVault(storagePath: string): Error {
|
|
143
|
+
return new Error(`fx.vault.get: missing per-tenant secret "${storagePath}"`);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Attach tenant auth methods and KV prefixing onto a live `fx` bag.
|
|
148
|
+
* Called from `createFx` only when tenancy is on.
|
|
149
|
+
*
|
|
150
|
+
* @param fx - Mutable fx bag
|
|
151
|
+
* @param ctx - Tenant id + createFx options + gates
|
|
152
|
+
*/
|
|
153
|
+
export function install(
|
|
154
|
+
fx: {
|
|
155
|
+
auth: FxAuthIdentity;
|
|
156
|
+
store: (ref: never) => object;
|
|
157
|
+
},
|
|
158
|
+
ctx: {
|
|
159
|
+
readonly tenantId: string | null;
|
|
160
|
+
readonly options: {
|
|
161
|
+
readonly tenantStore?: TenantStore;
|
|
162
|
+
readonly tenantEnabled?: boolean;
|
|
163
|
+
readonly sessions?: SessionStore;
|
|
164
|
+
readonly sessionCrypto?: SessionCrypto;
|
|
165
|
+
readonly manifest?: Manifest | null;
|
|
166
|
+
};
|
|
167
|
+
readonly now: () => number;
|
|
168
|
+
readonly gated: AttachAuthTenantMethodsOptions["gated"];
|
|
169
|
+
},
|
|
170
|
+
): void {
|
|
171
|
+
const next = bind(
|
|
172
|
+
fx.auth,
|
|
173
|
+
{
|
|
174
|
+
tenantStore: ctx.options.tenantStore,
|
|
175
|
+
sessions: ctx.options.sessions,
|
|
176
|
+
sessionCrypto: ctx.options.sessionCrypto,
|
|
177
|
+
manifest: ctx.options.manifest ?? undefined,
|
|
178
|
+
},
|
|
179
|
+
ctx.now,
|
|
180
|
+
ctx.gated,
|
|
181
|
+
);
|
|
182
|
+
fx.auth = next;
|
|
183
|
+
if (ctx.options.tenantEnabled !== true) return;
|
|
184
|
+
const inner = fx.store;
|
|
185
|
+
(fx as { store: typeof inner }).store = ((ref: never) => {
|
|
186
|
+
const handle = inner(ref);
|
|
187
|
+
if (
|
|
188
|
+
typeof ref === "object" &&
|
|
189
|
+
ref !== null &&
|
|
190
|
+
"facet" in ref &&
|
|
191
|
+
(ref as KvStoreDecl).facet === "kv"
|
|
192
|
+
) {
|
|
193
|
+
const decl = ref as KvStoreDecl;
|
|
194
|
+
return new Proxy(handle, {
|
|
195
|
+
get(target, prop, receiver) {
|
|
196
|
+
const val = Reflect.get(target, prop, receiver);
|
|
197
|
+
if (typeof val !== "function" || prop === "then") return val;
|
|
198
|
+
return (...args: unknown[]) => {
|
|
199
|
+
const callArgs = rewriteKvArgs(ctx.tenantId, decl, true, prop, args);
|
|
200
|
+
const result = (val as (...a: unknown[]) => unknown).apply(target, callArgs);
|
|
201
|
+
if (prop === "list") {
|
|
202
|
+
return Promise.resolve(result as Promise<string[]>).then((keys) =>
|
|
203
|
+
stripKvPrefix(ctx.tenantId, decl, true, keys),
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
return result;
|
|
207
|
+
};
|
|
208
|
+
},
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
return handle;
|
|
212
|
+
}) as typeof inner;
|
|
213
|
+
}
|
package/src/kernel/fx.test.ts
CHANGED
|
@@ -86,6 +86,52 @@ describe("fx — capability enforcement", () => {
|
|
|
86
86
|
expect(ttl).toBeGreaterThan(0);
|
|
87
87
|
});
|
|
88
88
|
|
|
89
|
+
test("kv prefixes keys with tenant id and fails loud without one", async () => {
|
|
90
|
+
const { createStoreRuntime, store } = await import("../elements/store.ts");
|
|
91
|
+
const { memoryKvDriver } = await import("../drivers/index.ts");
|
|
92
|
+
const decl = store.kv("drafts");
|
|
93
|
+
const rt = createStoreRuntime({
|
|
94
|
+
drivers: { kv: memoryKvDriver },
|
|
95
|
+
kv: { drafts: {} },
|
|
96
|
+
});
|
|
97
|
+
rt.register(decl);
|
|
98
|
+
const acme = createFx({
|
|
99
|
+
flow: "drafts.acme",
|
|
100
|
+
effects: { reads: ["kv:drafts"], writes: ["kv:drafts"] },
|
|
101
|
+
storeRuntime: rt,
|
|
102
|
+
tenantEnabled: true,
|
|
103
|
+
tenant: { id: "acme" },
|
|
104
|
+
});
|
|
105
|
+
const globex = createFx({
|
|
106
|
+
flow: "drafts.globex",
|
|
107
|
+
effects: { reads: ["kv:drafts"], writes: ["kv:drafts"] },
|
|
108
|
+
storeRuntime: rt,
|
|
109
|
+
tenantEnabled: true,
|
|
110
|
+
tenant: { id: "globex" },
|
|
111
|
+
});
|
|
112
|
+
await acme.store(decl).set("note", "alice");
|
|
113
|
+
await globex.store(decl).set("note", "bob");
|
|
114
|
+
expect(await acme.store(decl).get("note")).toBe("alice");
|
|
115
|
+
expect(await globex.store(decl).get("note")).toBe("bob");
|
|
116
|
+
expect(await acme.store(decl).list()).toEqual(["note"]);
|
|
117
|
+
|
|
118
|
+
const none = createFx({
|
|
119
|
+
flow: "drafts.none",
|
|
120
|
+
effects: { writes: ["kv:drafts"] },
|
|
121
|
+
storeRuntime: rt,
|
|
122
|
+
tenantEnabled: true,
|
|
123
|
+
tenant: { id: null },
|
|
124
|
+
});
|
|
125
|
+
let err: unknown;
|
|
126
|
+
try {
|
|
127
|
+
await none.store(decl).set("note", "x");
|
|
128
|
+
} catch (e) {
|
|
129
|
+
err = e;
|
|
130
|
+
}
|
|
131
|
+
expect(err).toBeInstanceOf(OkeError);
|
|
132
|
+
expect((err as OkeError).code).toBe(1015);
|
|
133
|
+
});
|
|
134
|
+
|
|
89
135
|
test("index driverId is meilisearch before first I/O (not a function, not memory)", async () => {
|
|
90
136
|
const { createStoreRuntime, store } = await import("../elements/store.ts");
|
|
91
137
|
const { meilisearchDriver } = await import("../drivers/index.ts");
|
|
@@ -334,6 +380,28 @@ describe("fx.vault — object surface", () => {
|
|
|
334
380
|
/fx\.vault\.status needs a bound Vault backend/,
|
|
335
381
|
);
|
|
336
382
|
});
|
|
383
|
+
|
|
384
|
+
test("per-tenant contracts store under {tenantId}/{contract}", async () => {
|
|
385
|
+
const { createVaultRuntime, vault } = await import("../elements/vault.ts");
|
|
386
|
+
const { adapter, rows } = memoryAdapter();
|
|
387
|
+
const stripe = vault.secret("STRIPE_KEY", { perTenant: true, dev: "sk_unused" });
|
|
388
|
+
const runtime = createVaultRuntime({
|
|
389
|
+
secrets: [stripe],
|
|
390
|
+
tenantEnabled: true,
|
|
391
|
+
});
|
|
392
|
+
const fx = createFx({
|
|
393
|
+
flow: "billing.key",
|
|
394
|
+
effects: { secrets: ["STRIPE_KEY"] },
|
|
395
|
+
vaultRuntime: runtime,
|
|
396
|
+
vaultAdapter: adapter as never,
|
|
397
|
+
tenantEnabled: true,
|
|
398
|
+
tenant: { id: "acme" },
|
|
399
|
+
});
|
|
400
|
+
await fx.vault.set("STRIPE_KEY", "sk_acme");
|
|
401
|
+
expect([...rows.keys()]).toEqual(["acme/STRIPE_KEY"]);
|
|
402
|
+
const redacted = await fx.vault.get("STRIPE_KEY");
|
|
403
|
+
expect(redacted.reveal()).toBe("sk_acme");
|
|
404
|
+
});
|
|
337
405
|
});
|
|
338
406
|
|
|
339
407
|
describe("fx — wholesale swap", () => {
|
|
@@ -364,6 +432,9 @@ describe("fx — wholesale swap", () => {
|
|
|
364
432
|
async deadLetters() {
|
|
365
433
|
return [];
|
|
366
434
|
},
|
|
435
|
+
live() {
|
|
436
|
+
throw new Error("live must not be used");
|
|
437
|
+
},
|
|
367
438
|
async call() {
|
|
368
439
|
return null;
|
|
369
440
|
},
|
|
@@ -432,7 +503,43 @@ describe("fx — wholesale swap", () => {
|
|
|
432
503
|
t: (key) => key,
|
|
433
504
|
locale: "en",
|
|
434
505
|
id: () => "fixed-id",
|
|
435
|
-
auth: {
|
|
506
|
+
auth: {
|
|
507
|
+
userId: "u1",
|
|
508
|
+
scopes: new Set(["a"]),
|
|
509
|
+
createApiKey: async () => {
|
|
510
|
+
throw new Error("auth.createApiKey must not be used");
|
|
511
|
+
},
|
|
512
|
+
listApiKeys: async () => [],
|
|
513
|
+
revokeApiKey: async () => {
|
|
514
|
+
throw new Error("auth.revokeApiKey must not be used");
|
|
515
|
+
},
|
|
516
|
+
rotateApiKey: async () => {
|
|
517
|
+
throw new Error("auth.rotateApiKey must not be used");
|
|
518
|
+
},
|
|
519
|
+
updateApiKey: async () => {
|
|
520
|
+
throw new Error("auth.updateApiKey must not be used");
|
|
521
|
+
},
|
|
522
|
+
listTenants: async () => [],
|
|
523
|
+
switchTenant: async () => {
|
|
524
|
+
throw new Error("auth.switchTenant must not be used");
|
|
525
|
+
},
|
|
526
|
+
createTenant: async () => {
|
|
527
|
+
throw new Error("auth.createTenant must not be used");
|
|
528
|
+
},
|
|
529
|
+
deleteTenant: async () => {
|
|
530
|
+
throw new Error("auth.deleteTenant must not be used");
|
|
531
|
+
},
|
|
532
|
+
addMember: async () => {
|
|
533
|
+
throw new Error("auth.addMember must not be used");
|
|
534
|
+
},
|
|
535
|
+
removeMember: async () => {
|
|
536
|
+
throw new Error("auth.removeMember must not be used");
|
|
537
|
+
},
|
|
538
|
+
listMembers: async () => [],
|
|
539
|
+
upsertTenantRole: async () => {
|
|
540
|
+
throw new Error("auth.upsertTenantRole must not be used");
|
|
541
|
+
},
|
|
542
|
+
},
|
|
436
543
|
operator: { id: null },
|
|
437
544
|
principal: { userId: "u1", operatorId: null, scopes: new Set(["a"]) },
|
|
438
545
|
tenant: { id: "t1" },
|