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/auth/auth.test.ts
CHANGED
|
@@ -114,6 +114,62 @@ describe("auth plugin", () => {
|
|
|
114
114
|
});
|
|
115
115
|
});
|
|
116
116
|
|
|
117
|
+
describe("API key allowlist", () => {
|
|
118
|
+
test("hostname entry matches the resolved client IP", async () => {
|
|
119
|
+
const { authenticateApiKey } = await import("./api-keys.ts");
|
|
120
|
+
const store = createApiKeyStore();
|
|
121
|
+
const created = await createApiKey(store, {
|
|
122
|
+
plane: "user",
|
|
123
|
+
name: "ci",
|
|
124
|
+
scopes: ["member"],
|
|
125
|
+
creatorId: "u1",
|
|
126
|
+
creatorScopes: ["member"],
|
|
127
|
+
ipAllowlist: ["ci.example.com"],
|
|
128
|
+
});
|
|
129
|
+
const lookup = async (host: string) => (host === "ci.example.com" ? ["203.0.113.10"] : []);
|
|
130
|
+
expect(
|
|
131
|
+
await authenticateApiKey(store, created.secret, {
|
|
132
|
+
ip: "203.0.113.10",
|
|
133
|
+
lookup,
|
|
134
|
+
}),
|
|
135
|
+
).not.toBeNull();
|
|
136
|
+
expect(
|
|
137
|
+
await authenticateApiKey(store, created.secret, {
|
|
138
|
+
ip: "198.51.100.7",
|
|
139
|
+
lookup,
|
|
140
|
+
}),
|
|
141
|
+
).toBeNull();
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
test("hostname lookup failure is closed", async () => {
|
|
145
|
+
const { authenticateApiKey } = await import("./api-keys.ts");
|
|
146
|
+
const store = createApiKeyStore();
|
|
147
|
+
const created = await createApiKey(store, {
|
|
148
|
+
plane: "user",
|
|
149
|
+
name: "ci",
|
|
150
|
+
scopes: ["member"],
|
|
151
|
+
creatorId: "u1",
|
|
152
|
+
creatorScopes: ["member"],
|
|
153
|
+
ipAllowlist: ["ci.example.com", "203.0.113.10"],
|
|
154
|
+
});
|
|
155
|
+
const lookup = async () => {
|
|
156
|
+
throw new Error("nxdomain");
|
|
157
|
+
};
|
|
158
|
+
expect(
|
|
159
|
+
await authenticateApiKey(store, created.secret, {
|
|
160
|
+
ip: "203.0.113.10",
|
|
161
|
+
lookup,
|
|
162
|
+
}),
|
|
163
|
+
).not.toBeNull();
|
|
164
|
+
expect(
|
|
165
|
+
await authenticateApiKey(store, created.secret, {
|
|
166
|
+
ip: "198.51.100.7",
|
|
167
|
+
lookup,
|
|
168
|
+
}),
|
|
169
|
+
).toBeNull();
|
|
170
|
+
});
|
|
171
|
+
});
|
|
172
|
+
|
|
117
173
|
describe("API key attenuation", () => {
|
|
118
174
|
test("key cannot be created with a scope its creator lacks", async () => {
|
|
119
175
|
const store = createApiKeyStore();
|
package/src/auth/config.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
import type { PasswordHashOptions } from "../runtime/types.ts";
|
|
8
8
|
import type { SessionStore } from "./sessions.ts";
|
|
9
|
-
import type
|
|
9
|
+
import { createApiKeyStore, type ApiKeyStore } from "./api-keys.ts";
|
|
10
10
|
import type { BreachCheckFn } from "./breach-check.ts";
|
|
11
11
|
import type { PasswordPolicyOptions } from "./password-policy.ts";
|
|
12
12
|
import {
|
|
@@ -16,6 +16,12 @@ import {
|
|
|
16
16
|
type ResolvedAuthSchema,
|
|
17
17
|
} from "./schema.ts";
|
|
18
18
|
import { sessionCryptoFromAuthOptions, type AuthSessionOptions } from "./plugin.ts";
|
|
19
|
+
import { createTenantStore, type TenantStore } from "./tenants.ts";
|
|
20
|
+
import {
|
|
21
|
+
resolveTenantAuth,
|
|
22
|
+
type ResolvedTenantAuth,
|
|
23
|
+
type TenantAuthOptions,
|
|
24
|
+
} from "./tenant-config.ts";
|
|
19
25
|
|
|
20
26
|
/** Email + password method knobs. */
|
|
21
27
|
export interface EmailAndPasswordOptions {
|
|
@@ -111,10 +117,19 @@ export interface GateAuthOptions extends AuthSchemaOptions {
|
|
|
111
117
|
*/
|
|
112
118
|
readonly sessions?: SessionStore;
|
|
113
119
|
/**
|
|
114
|
-
*
|
|
115
|
-
*
|
|
120
|
+
* Shared API key store. Created automatically when omitted
|
|
121
|
+
* (`gate.auth.secret` is the HMAC pepper).
|
|
116
122
|
*/
|
|
117
123
|
readonly apiKeyStore?: ApiKeyStore;
|
|
124
|
+
/**
|
|
125
|
+
* Multi-tenancy as an identity dimension. `true` enables defaults
|
|
126
|
+
* (`source: "claim"`, `required: false`). Off when omitted.
|
|
127
|
+
*/
|
|
128
|
+
readonly tenant?: boolean | TenantAuthOptions;
|
|
129
|
+
/**
|
|
130
|
+
* Shared tenant registry. Created automatically when {@link tenant} is on.
|
|
131
|
+
*/
|
|
132
|
+
readonly tenantStore?: TenantStore;
|
|
118
133
|
/** Injectable clock. */
|
|
119
134
|
readonly now?: () => number;
|
|
120
135
|
}
|
|
@@ -170,7 +185,9 @@ export interface ResolvedGateAuth {
|
|
|
170
185
|
};
|
|
171
186
|
readonly hooks: AuthDatabaseHooks | undefined;
|
|
172
187
|
readonly sessions: SessionStore | undefined;
|
|
173
|
-
readonly apiKeyStore: ApiKeyStore
|
|
188
|
+
readonly apiKeyStore: ApiKeyStore;
|
|
189
|
+
readonly tenant: ResolvedTenantAuth | undefined;
|
|
190
|
+
readonly tenantStore: TenantStore | undefined;
|
|
174
191
|
readonly now: (() => number) | undefined;
|
|
175
192
|
}
|
|
176
193
|
|
|
@@ -278,7 +295,9 @@ export function resolveGateAuth(options: ResolveGateAuthOptions): ResolvedGateAu
|
|
|
278
295
|
},
|
|
279
296
|
hooks: auth.hooks,
|
|
280
297
|
sessions: auth.sessions,
|
|
281
|
-
apiKeyStore: auth.apiKeyStore,
|
|
298
|
+
apiKeyStore: auth.apiKeyStore ?? createApiKeyStore({ pepper: secret }),
|
|
299
|
+
tenant: auth.tenant ? resolveTenantAuth(auth.tenant) : undefined,
|
|
300
|
+
tenantStore: auth.tenant ? (auth.tenantStore ?? createTenantStore()) : auth.tenantStore,
|
|
282
301
|
now: auth.now,
|
|
283
302
|
};
|
|
284
303
|
}
|
package/src/auth/index.ts
CHANGED
|
@@ -161,17 +161,41 @@ export {
|
|
|
161
161
|
} from "./attenuation.ts";
|
|
162
162
|
|
|
163
163
|
export {
|
|
164
|
+
AUTH_API_KEYS_RESOURCE,
|
|
165
|
+
allowlistAllowsIp,
|
|
166
|
+
authenticateApiKey,
|
|
167
|
+
clientIpFromRequest,
|
|
164
168
|
createApiKey,
|
|
165
169
|
createApiKeyStore,
|
|
166
|
-
|
|
170
|
+
getApiKey,
|
|
167
171
|
hashApiKeySecret,
|
|
172
|
+
listApiKeys,
|
|
168
173
|
revokeApiKey,
|
|
169
174
|
rotateApiKey,
|
|
175
|
+
toApiKeyPublicRow,
|
|
176
|
+
updateApiKey,
|
|
177
|
+
type AllowlistLookup,
|
|
178
|
+
type ApiKeyPublicRow,
|
|
170
179
|
type ApiKeyStore,
|
|
180
|
+
type AuthenticateApiKeyOptions,
|
|
171
181
|
type CreateApiKeyOptions,
|
|
182
|
+
type CreateApiKeyStoreOptions,
|
|
172
183
|
type CreatedApiKey,
|
|
184
|
+
type UpdateApiKeyOptions,
|
|
173
185
|
} from "./api-keys.ts";
|
|
174
186
|
|
|
187
|
+
export {
|
|
188
|
+
apiKeySqlExec,
|
|
189
|
+
bindApiKeySqlPersist,
|
|
190
|
+
bindHostApiKeySql,
|
|
191
|
+
bindHostApiKeySqlFromStore,
|
|
192
|
+
ensureApiKeyTable,
|
|
193
|
+
hydrateApiKeyStore,
|
|
194
|
+
persistApiKeyRow,
|
|
195
|
+
type ApiKeySqlExec,
|
|
196
|
+
type HostApiKeySqlRuntime,
|
|
197
|
+
} from "./api-key-sql.ts";
|
|
198
|
+
|
|
175
199
|
export {
|
|
176
200
|
createOperatorInviteStore,
|
|
177
201
|
createOperatorInvite,
|
|
@@ -236,4 +260,41 @@ export {
|
|
|
236
260
|
type RoleRow,
|
|
237
261
|
type SessionRow,
|
|
238
262
|
type TablePlane,
|
|
263
|
+
type TenantMemberRow,
|
|
264
|
+
type TenantRoleRow,
|
|
265
|
+
type TenantRow,
|
|
239
266
|
} from "./tables.ts";
|
|
267
|
+
|
|
268
|
+
export { AUTH_TENANT_TABLES } from "./tenant-tables.ts";
|
|
269
|
+
|
|
270
|
+
export {
|
|
271
|
+
AUTH_TENANTS_RESOURCE,
|
|
272
|
+
DEFAULT_TENANT_ROLE,
|
|
273
|
+
TenantError,
|
|
274
|
+
addMember,
|
|
275
|
+
createTenant,
|
|
276
|
+
createTenantStore,
|
|
277
|
+
deleteTenant,
|
|
278
|
+
getMember,
|
|
279
|
+
isMember,
|
|
280
|
+
listMembers,
|
|
281
|
+
listTenantIds,
|
|
282
|
+
listTenantsForUser,
|
|
283
|
+
removeMember,
|
|
284
|
+
tenantRoleScopeFailure,
|
|
285
|
+
tenantScopesForMember,
|
|
286
|
+
upsertTenantRole,
|
|
287
|
+
type CreateTenantOptions,
|
|
288
|
+
type TenantPublicRow,
|
|
289
|
+
type TenantStore,
|
|
290
|
+
type TenantStoreHooks,
|
|
291
|
+
type UpsertTenantRoleOptions,
|
|
292
|
+
} from "./tenants.ts";
|
|
293
|
+
|
|
294
|
+
export {
|
|
295
|
+
resolveTenantAuth,
|
|
296
|
+
type ResolvedTenantAuth,
|
|
297
|
+
type TenantAuthOptions,
|
|
298
|
+
type TenantResolveContext,
|
|
299
|
+
type TenantSource,
|
|
300
|
+
} from "./tenant-config.ts";
|
package/src/auth/plugin.ts
CHANGED
|
@@ -9,6 +9,7 @@ import { plugin, type PluginDef } from "../kernel/plugin.ts";
|
|
|
9
9
|
import type { BreachCheckFn } from "./breach-check.ts";
|
|
10
10
|
import type { PasswordPolicyOptions } from "./password-policy.ts";
|
|
11
11
|
import { AUTH_TABLES } from "./tables.ts";
|
|
12
|
+
import { AUTH_TENANT_TABLES } from "./tenant-tables.ts";
|
|
12
13
|
|
|
13
14
|
/** Session knobs beyond raw access/refresh TTLs. */
|
|
14
15
|
export interface AuthSessionOptions {
|
|
@@ -83,7 +84,11 @@ export function auth(options: AuthPluginOptions = {}): PluginDef {
|
|
|
83
84
|
/* principals resolved by session / API key middleware at runtime */
|
|
84
85
|
});
|
|
85
86
|
|
|
86
|
-
|
|
87
|
+
const tables: readonly string[] = [
|
|
88
|
+
...Object.values(AUTH_TABLES),
|
|
89
|
+
...Object.values(AUTH_TENANT_TABLES),
|
|
90
|
+
];
|
|
91
|
+
for (const name of tables) {
|
|
87
92
|
builder = builder.table(name, undefined, {
|
|
88
93
|
plane: name.includes("operator")
|
|
89
94
|
? "operator"
|
package/src/auth/schema.ts
CHANGED
|
@@ -105,6 +105,10 @@ export const AUTH_MODEL_DEFAULT_FIELDS: Readonly<
|
|
|
105
105
|
name: "name",
|
|
106
106
|
scopes: "scopes",
|
|
107
107
|
expiresAt: "expires_at",
|
|
108
|
+
rateLimit: "rate_limit",
|
|
109
|
+
ipAllowlist: "ip_allowlist",
|
|
110
|
+
creatorId: "creator_id",
|
|
111
|
+
creatorScopes: "creator_scopes",
|
|
108
112
|
createdAt: "created_at",
|
|
109
113
|
lastUsedAt: "last_used_at",
|
|
110
114
|
revokedAt: "revoked_at",
|
|
@@ -190,6 +194,10 @@ const CORE_SQL_TYPES: Readonly<Record<string, AuthColumnSqlType>> = {
|
|
|
190
194
|
scopes: "TEXT",
|
|
191
195
|
audience: "TEXT",
|
|
192
196
|
lastUsedAt: "INTEGER",
|
|
197
|
+
rateLimit: "TEXT",
|
|
198
|
+
ipAllowlist: "TEXT",
|
|
199
|
+
creatorId: "TEXT",
|
|
200
|
+
creatorScopes: "TEXT",
|
|
193
201
|
};
|
|
194
202
|
|
|
195
203
|
/**
|
package/src/auth/sessions.ts
CHANGED
|
@@ -43,6 +43,11 @@ export interface AccessClaims {
|
|
|
43
43
|
* Validated when the verifier supplies {@link VerifyAccessOptions.audience}.
|
|
44
44
|
*/
|
|
45
45
|
readonly aud?: string;
|
|
46
|
+
/**
|
|
47
|
+
* Active tenant for this access token (request-scoped).
|
|
48
|
+
* Never stored on the session row — switching issues a new token pair.
|
|
49
|
+
*/
|
|
50
|
+
readonly tid?: string;
|
|
46
51
|
}
|
|
47
52
|
|
|
48
53
|
/** Options for {@link createSessionStore}. */
|
|
@@ -106,6 +111,8 @@ export async function issueSession(
|
|
|
106
111
|
readonly id: string;
|
|
107
112
|
readonly plane: AuthPlane;
|
|
108
113
|
readonly scopes: Iterable<string>;
|
|
114
|
+
/** Optional tenant claim (not stored on the session row). */
|
|
115
|
+
readonly tenantId?: string | null;
|
|
109
116
|
},
|
|
110
117
|
): Promise<IssuedSession> {
|
|
111
118
|
const now = crypto.now ?? (() => Date.now());
|
|
@@ -136,6 +143,7 @@ export async function issueSession(
|
|
|
136
143
|
store.sessions.set(sessionId, session);
|
|
137
144
|
|
|
138
145
|
const refreshRaw = `rt_${cryptoRandomId()}`;
|
|
146
|
+
const tid = principal.tenantId ?? undefined;
|
|
139
147
|
const refreshRow: RefreshTokenRow = {
|
|
140
148
|
id: cryptoRandomId(),
|
|
141
149
|
sessionId,
|
|
@@ -144,6 +152,7 @@ export async function issueSession(
|
|
|
144
152
|
expiresAt: t + refreshTtl,
|
|
145
153
|
usedAt: null,
|
|
146
154
|
revokedAt: null,
|
|
155
|
+
...(tid !== undefined && tid !== "" ? { tid } : {}),
|
|
147
156
|
};
|
|
148
157
|
store.refresh.set(refreshRow.id, refreshRow);
|
|
149
158
|
|
|
@@ -156,6 +165,7 @@ export async function issueSession(
|
|
|
156
165
|
iat: t,
|
|
157
166
|
exp: accessExpiresAt,
|
|
158
167
|
...(crypto.audience !== undefined ? { aud: crypto.audience } : {}),
|
|
168
|
+
...(tid !== undefined && tid !== "" ? { tid } : {}),
|
|
159
169
|
});
|
|
160
170
|
|
|
161
171
|
return { session, accessToken, refreshToken: refreshRaw, accessExpiresAt };
|
|
@@ -211,6 +221,7 @@ export async function rotateRefresh(
|
|
|
211
221
|
const refreshTtl = crypto.refreshTtlMs ?? REFRESH_TTL_MS;
|
|
212
222
|
|
|
213
223
|
const newRefreshRaw = `rt_${cryptoRandomId()}`;
|
|
224
|
+
const rotatedTid = existing.tid;
|
|
214
225
|
const newRefresh: RefreshTokenRow = {
|
|
215
226
|
id: cryptoRandomId(),
|
|
216
227
|
sessionId: session.id,
|
|
@@ -219,6 +230,9 @@ export async function rotateRefresh(
|
|
|
219
230
|
expiresAt: t + refreshTtl,
|
|
220
231
|
usedAt: null,
|
|
221
232
|
revokedAt: null,
|
|
233
|
+
...(rotatedTid !== undefined && rotatedTid !== null && rotatedTid !== ""
|
|
234
|
+
? { tid: rotatedTid }
|
|
235
|
+
: {}),
|
|
222
236
|
};
|
|
223
237
|
store.refresh.set(newRefresh.id, newRefresh);
|
|
224
238
|
|
|
@@ -231,6 +245,9 @@ export async function rotateRefresh(
|
|
|
231
245
|
iat: t,
|
|
232
246
|
exp: accessExpiresAt,
|
|
233
247
|
...(session.audience !== undefined ? { aud: session.audience } : {}),
|
|
248
|
+
...(rotatedTid !== undefined && rotatedTid !== null && rotatedTid !== ""
|
|
249
|
+
? { tid: rotatedTid }
|
|
250
|
+
: {}),
|
|
234
251
|
});
|
|
235
252
|
|
|
236
253
|
return {
|
|
@@ -291,6 +308,7 @@ export async function issueSessionWithScopes(
|
|
|
291
308
|
readonly id: string;
|
|
292
309
|
readonly plane: AuthPlane;
|
|
293
310
|
readonly scopes: Iterable<string>;
|
|
311
|
+
readonly tenantId?: string | null;
|
|
294
312
|
},
|
|
295
313
|
): Promise<IssuedSession> {
|
|
296
314
|
return issueSession(store, crypto, principal);
|
package/src/auth/tables.ts
CHANGED
|
@@ -98,6 +98,8 @@ export interface ApiKeyRow {
|
|
|
98
98
|
lastUsedAt: number | null;
|
|
99
99
|
/** Epoch-ms when revoked; `null` while active. */
|
|
100
100
|
revokedAt: number | null;
|
|
101
|
+
/** Optional tenant claim (tier-1 API-key tenancy). */
|
|
102
|
+
tenantId?: string | null;
|
|
101
103
|
}
|
|
102
104
|
|
|
103
105
|
/** Pending operator invitation (invite-only plane). */
|
|
@@ -136,4 +138,34 @@ export interface RefreshTokenRow {
|
|
|
136
138
|
expiresAt: number;
|
|
137
139
|
usedAt: number | null;
|
|
138
140
|
revokedAt: number | null;
|
|
141
|
+
/** Tenant id stamped on this refresh row (request-scoped; not on the session). */
|
|
142
|
+
tid?: string | null;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** Tenant registry row. */
|
|
146
|
+
export interface TenantRow {
|
|
147
|
+
id: string;
|
|
148
|
+
name: string;
|
|
149
|
+
slug: string | null;
|
|
150
|
+
createdAt: number;
|
|
151
|
+
createdBy: string;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** N:N membership — one user, many tenants; one tenant, many users. */
|
|
155
|
+
export interface TenantMemberRow {
|
|
156
|
+
id: string;
|
|
157
|
+
tenantId: string;
|
|
158
|
+
userId: string;
|
|
159
|
+
/** Tenant role name (expanded via {@link TenantRoleRow}). */
|
|
160
|
+
role: string;
|
|
161
|
+
createdAt: number;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** Tenant-scoped role → application scope names. */
|
|
165
|
+
export interface TenantRoleRow {
|
|
166
|
+
tenantId: string;
|
|
167
|
+
roleName: string;
|
|
168
|
+
scopes: string[];
|
|
169
|
+
createdAt: number;
|
|
170
|
+
updatedAt: number;
|
|
139
171
|
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `gate.auth.tenant` config — identity dimension opt-in.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/** How the request names a tenant. */
|
|
6
|
+
export type TenantSource = "claim" | "header" | "subdomain" | "resolve";
|
|
7
|
+
|
|
8
|
+
/** Public `gate.auth.tenant` bag (boolean shorthand or options). */
|
|
9
|
+
export interface TenantAuthOptions {
|
|
10
|
+
/**
|
|
11
|
+
* Pure B2B: every user-plane request needs a tenant.
|
|
12
|
+
* Default `false` (B2C+B2B coexistence).
|
|
13
|
+
*/
|
|
14
|
+
readonly required?: boolean;
|
|
15
|
+
/** Resolution source. Default `"claim"`. */
|
|
16
|
+
readonly source?: TenantSource;
|
|
17
|
+
/** Header name for `source: "header"`. Default `x-oke-tenant`. */
|
|
18
|
+
readonly header?: string;
|
|
19
|
+
/**
|
|
20
|
+
* Tier-3 escape hatch. When it returns an id, membership is still
|
|
21
|
+
* checked unless {@link TenantAuthOptions.authoritative} is true.
|
|
22
|
+
*/
|
|
23
|
+
readonly resolve?: (ctx: TenantResolveContext) => string | null | undefined;
|
|
24
|
+
/**
|
|
25
|
+
* When true, {@link TenantAuthOptions.resolve} is trusted without a
|
|
26
|
+
* membership query. Default false (fail-safe).
|
|
27
|
+
*/
|
|
28
|
+
readonly authoritative?: boolean;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Context passed to {@link TenantAuthOptions.resolve}. */
|
|
32
|
+
export interface TenantResolveContext {
|
|
33
|
+
readonly auth: {
|
|
34
|
+
readonly userId: string | null;
|
|
35
|
+
readonly tenantId?: string | null;
|
|
36
|
+
};
|
|
37
|
+
readonly request?: Request;
|
|
38
|
+
readonly claimTenantId: string | null;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Fully resolved tenant auth config. */
|
|
42
|
+
export interface ResolvedTenantAuth {
|
|
43
|
+
readonly enabled: true;
|
|
44
|
+
readonly required: boolean;
|
|
45
|
+
readonly source: TenantSource;
|
|
46
|
+
readonly header: string;
|
|
47
|
+
readonly resolve?: TenantAuthOptions["resolve"];
|
|
48
|
+
readonly authoritative: boolean;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Normalize `gate.auth.tenant`.
|
|
53
|
+
*
|
|
54
|
+
* @param input - `true` or options bag
|
|
55
|
+
*/
|
|
56
|
+
export function resolveTenantAuth(input: true | TenantAuthOptions): ResolvedTenantAuth {
|
|
57
|
+
if (input === true) {
|
|
58
|
+
return {
|
|
59
|
+
enabled: true,
|
|
60
|
+
required: false,
|
|
61
|
+
source: "claim",
|
|
62
|
+
header: "x-oke-tenant",
|
|
63
|
+
authoritative: false,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
return {
|
|
67
|
+
enabled: true,
|
|
68
|
+
required: input.required === true,
|
|
69
|
+
source: input.source ?? "claim",
|
|
70
|
+
header: input.header ?? "x-oke-tenant",
|
|
71
|
+
...(input.resolve !== undefined ? { resolve: input.resolve } : {}),
|
|
72
|
+
authoritative: input.authoritative === true,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tenant registry table names — kept off AUTH_TABLES so Store-only graphs
|
|
3
|
+
* that already ship `oke_api_keys` do not also pin `oke_tenants`.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/** Opt-in via `gate.auth.tenant`. */
|
|
7
|
+
export const AUTH_TENANT_TABLES = {
|
|
8
|
+
tenants: "oke_tenants",
|
|
9
|
+
tenantMembers: "oke_tenant_members",
|
|
10
|
+
tenantRoles: "oke_tenant_roles",
|
|
11
|
+
} as const;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tenant registry — user-plane catalog on write; membership helpers.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, expect, test } from "bun:test";
|
|
6
|
+
import { isApplicationScope } from "../elements/gate/permissions.ts";
|
|
7
|
+
import {
|
|
8
|
+
createTenant,
|
|
9
|
+
createTenantStore,
|
|
10
|
+
tenantRoleScopeFailure,
|
|
11
|
+
upsertTenantRole,
|
|
12
|
+
} from "./tenants.ts";
|
|
13
|
+
|
|
14
|
+
describe("tenantRoleScopeFailure", () => {
|
|
15
|
+
const catalog = ["booking:create", "member", "console:store.sql:write", "console:*"];
|
|
16
|
+
|
|
17
|
+
test("unknown names and console:* fail the same way", () => {
|
|
18
|
+
const invented = tenantRoleScopeFailure(["this:was:never:declared"], catalog);
|
|
19
|
+
const consoleWrite = tenantRoleScopeFailure(["console:store.sql:write"], catalog);
|
|
20
|
+
const consoleStar = tenantRoleScopeFailure(["console:*"], catalog);
|
|
21
|
+
expect(invented?.reason).toBe("unknown_scope");
|
|
22
|
+
expect(consoleWrite?.reason).toBe("unknown_scope");
|
|
23
|
+
expect(consoleStar?.reason).toBe("unknown_scope");
|
|
24
|
+
expect(isApplicationScope("console:store.sql:write")).toBe(false);
|
|
25
|
+
expect(isApplicationScope("console:*")).toBe(false);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
test("application catalog names are grantable", () => {
|
|
29
|
+
expect(tenantRoleScopeFailure(["booking:create", "member"], catalog)).toBeNull();
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
describe("upsertTenantRole", () => {
|
|
34
|
+
test("console-shaped upsert throws like an invented name", async () => {
|
|
35
|
+
const store = createTenantStore();
|
|
36
|
+
await createTenant(store, { name: "Acme", createdBy: "u1", id: "t1" });
|
|
37
|
+
const catalog = ["booking:create", "console:store.sql:write", "console:*"];
|
|
38
|
+
expect(() =>
|
|
39
|
+
upsertTenantRole(store, {
|
|
40
|
+
tenantId: "t1",
|
|
41
|
+
roleName: "admin",
|
|
42
|
+
scopes: ["console:store.sql:write"],
|
|
43
|
+
catalog,
|
|
44
|
+
}),
|
|
45
|
+
).toThrow(/unknown or operator-plane scope/);
|
|
46
|
+
expect(() =>
|
|
47
|
+
upsertTenantRole(store, {
|
|
48
|
+
tenantId: "t1",
|
|
49
|
+
roleName: "admin",
|
|
50
|
+
scopes: ["console:*"],
|
|
51
|
+
catalog,
|
|
52
|
+
}),
|
|
53
|
+
).toThrow(/unknown or operator-plane scope/);
|
|
54
|
+
expect(() =>
|
|
55
|
+
upsertTenantRole(store, {
|
|
56
|
+
tenantId: "t1",
|
|
57
|
+
roleName: "admin",
|
|
58
|
+
scopes: ["this:was:never:declared"],
|
|
59
|
+
catalog,
|
|
60
|
+
}),
|
|
61
|
+
).toThrow(/unknown or operator-plane scope/);
|
|
62
|
+
});
|
|
63
|
+
});
|