okengine 0.17.2 → 0.18.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -5
- package/manifest.v1.schema.json +61 -2
- package/package.json +18 -4
- package/site/content/docs/elements/clock.mdx +1 -1
- package/site/content/docs/elements/flow.mdx +25 -1
- package/site/content/docs/elements/store.mdx +287 -341
- package/site/content/docs/elements/vault.mdx +5 -5
- package/site/content/docs/get-started/installation.mdx +1 -2
- package/site/content/docs/get-started/introduction.mdx +54 -110
- package/site/content/docs/get-started/meta.json +9 -1
- package/site/content/docs/get-started/testing.mdx +328 -0
- package/site/content/docs/get-started/why.mdx +94 -70
- package/site/content/docs/index.mdx +1 -1
- package/site/content/docs/plugins/apple.mdx +151 -0
- package/site/content/docs/plugins/discord.mdx +139 -0
- package/site/content/docs/plugins/facebook.mdx +134 -0
- package/site/content/docs/plugins/figma.mdx +138 -0
- package/site/content/docs/plugins/github.mdx +138 -0
- package/site/content/docs/plugins/google.mdx +153 -0
- package/site/content/docs/plugins/index.mdx +47 -1
- package/site/content/docs/plugins/meta.json +10 -0
- package/site/content/docs/plugins/microsoft.mdx +151 -0
- package/site/content/docs/plugins/oauth.mdx +188 -0
- package/site/content/docs/plugins/x.mdx +125 -0
- package/site/content/docs/reference/cli.md +3 -2
- package/site/content/docs/reference/client.mdx +58 -1
- package/site/content/docs/reference/configuration.mdx +2 -4
- package/site/content/docs/reference/fx.mdx +3 -1
- package/site/content/docs/reference/index.mdx +0 -5
- package/site/content/docs/reference/meta.json +2 -2
- package/site/content/docs/reference/okid.mdx +137 -0
- package/src/auth/bindings.ts +1 -1
- package/src/auth/config.ts +9 -0
- package/src/auth/identity-sql.ts +314 -0
- package/src/auth/identity.ts +140 -2
- package/src/auth/index.ts +17 -1
- package/src/auth/method-context.ts +3 -0
- package/src/auth/oauth-as/cimd.ts +132 -0
- package/src/auth/oauth-as/crypto.test.ts +101 -0
- package/src/auth/oauth-as/crypto.ts +393 -0
- package/src/auth/oauth-as/errors.ts +68 -0
- package/src/auth/oauth-as/http.test.ts +419 -0
- package/src/auth/oauth-as/http.ts +842 -0
- package/src/auth/oauth-as/stores.ts +61 -0
- package/src/auth/oauth-as/tables.ts +142 -0
- package/src/auth/tables.ts +0 -11
- package/src/bench/README.md +83 -0
- package/src/bench/REPORT.md +176 -0
- package/src/bench/g01-rls-stamp.bench.ts +194 -0
- package/src/bench/g02-clock-per-tenant.bench.ts +158 -0
- package/src/bench/g03-signal-once.bench.ts +157 -0
- package/src/bench/g03-signal-reconnect.bench.ts +254 -0
- package/src/bench/g03-signal-sse-memory.bench.ts +191 -0
- package/src/bench/g04-auth-vault-hotpath.bench.ts +170 -0
- package/src/bench/g05-sustained-full.bench.ts +265 -0
- package/src/bench/g06-mixed-load.bench.ts +260 -0
- package/src/bench/g07-vault-crypto.bench.ts +100 -0
- package/src/bench/g07-vault-rotate-under-read.bench.ts +285 -0
- package/src/bench/g08-conn-oversubscribe.bench.ts +194 -0
- package/src/bench/g08-store-kv-durable.bench.ts +133 -0
- package/src/bench/g08-store-sql.bench.ts +178 -0
- package/src/bench/g09-journal-sustained.bench.ts +203 -0
- package/src/bench/g10-observability-contention.bench.ts +246 -0
- package/src/bench/g11-cold-start-cycle.bench.ts +164 -0
- package/src/bench/g13-elements.bench.ts +427 -0
- package/src/bench/g14-graceful-shutdown.bench.ts +244 -0
- package/src/bench/g15-postgres-degradation.bench.ts +264 -0
- package/src/bench/g16-live-query-fanout.bench.ts +206 -0
- package/src/bench/lib/event-loop-lag.ts +26 -0
- package/src/bench/lib/infra.ts +60 -0
- package/src/bench/lib/report.ts +52 -0
- package/src/bench/lib/rss-sampler.ts +61 -0
- package/src/bench/lib/signal-pg.ts +88 -0
- package/src/bench/load-app.ts +337 -0
- package/src/bench/load-child.ts +108 -0
- package/src/bench/smoke.bench.ts +43 -0
- package/src/cli/competitor-mention-removal.test.ts +28 -0
- package/src/cli/doctor-fd.ts +117 -0
- package/src/cli/doctor.test.ts +192 -0
- package/src/cli/doctor.ts +129 -1
- package/src/client/create.ts +95 -1
- package/src/client/index.ts +9 -2
- package/src/client/transport.ts +11 -4
- package/src/client/use-live-query.ts +154 -0
- package/src/client-react/index.ts +15 -1
- package/src/client-react/live-resource.ts +246 -0
- package/src/client-react/use-live-query.test.ts +475 -0
- package/src/client-react/use-live-query.ts +530 -0
- package/src/compiler/extract.test.ts +518 -0
- package/src/compiler/extract.ts +386 -19
- package/src/console/server/invoke-user-flow.ts +2 -1
- package/src/console/ui-next/dist/assets/{access-page-DnWbnGzq.js → access-page-DY4N6nnk.js} +1 -1
- package/src/console/ui-next/dist/assets/{flows-page-BiZ4-6yQ.js → flows-page-CsPDMrVM.js} +1 -1
- package/src/console/ui-next/dist/assets/{index-C8NRK2R-.js → index-CcTDXHuz.js} +3 -3
- package/src/console/ui-next/dist/assets/{observability-page-CrB6vd1T.js → observability-page-CKR595wP.js} +1 -1
- package/src/console/ui-next/dist/assets/{store-page-CS5-aETQ.js → store-page-02xOiqIK.js} +3 -3
- package/src/console/ui-next/dist/assets/{units-page-CjtdlW8l.js → units-page-CpPFFKyE.js} +1 -1
- package/src/console/ui-next/dist/assets/{vault-page-C6Xxm9SA.js → vault-page-BsMf-9_W.js} +1 -1
- package/src/console/ui-next/dist/index.html +1 -1
- package/src/console/ui-next/src/features/store/lib/fields-from-table.ts +36 -2
- package/src/drivers/cdc-outbox.ts +389 -0
- package/src/drivers/memory.ts +20 -0
- package/src/drivers/oauth-apple.ts +156 -0
- package/src/drivers/oauth-discord.ts +79 -0
- package/src/drivers/oauth-facebook.ts +80 -0
- package/src/drivers/oauth-figma.ts +116 -0
- package/src/drivers/oauth-github.ts +92 -0
- package/src/drivers/oauth-google.ts +142 -0
- package/src/drivers/oauth-microsoft.ts +174 -0
- package/src/drivers/oauth-oidc.ts +293 -0
- package/src/drivers/oauth-shared.ts +326 -0
- package/src/drivers/oauth-types.ts +159 -0
- package/src/drivers/oauth-x.ts +77 -0
- package/src/drivers/oauth2-common.ts +95 -0
- package/src/drivers/oauth2-token.ts +61 -0
- package/src/drivers/pg-rls-row-passes.ts +251 -0
- package/src/drivers/pg-rls.ts +2 -0
- package/src/drivers/postgres.ts +45 -2
- package/src/drivers/signal-postgres.ts +2 -1
- package/src/elements/channel/runtime.ts +29 -2
- package/src/elements/channel.test.ts +52 -0
- package/src/elements/gate/boot.ts +29 -2
- package/src/elements/store/emit-drizzle.ts +147 -14
- package/src/elements/store/field-ddl.test.ts +118 -0
- package/src/elements/store/field-types.test.ts +455 -0
- package/src/elements/store/list-query.golden.json +777 -0
- package/src/elements/store/list-query.parity.test.ts +396 -0
- package/src/elements/store/list-query.ts +792 -0
- package/src/elements/store/live-default.test.ts +136 -0
- package/src/elements/store/live-http.test.ts +160 -0
- package/src/elements/store/live-isolation.test.ts +291 -0
- package/src/elements/store/live-query-runtime.test.ts +323 -0
- package/src/elements/store/live-query-runtime.ts +403 -0
- package/src/elements/store/live-query-server.test.ts +377 -0
- package/src/elements/store/live-query-server.ts +102 -0
- package/src/elements/store/live-query.ts +97 -0
- package/src/elements/store/resource.ts +189 -680
- package/src/elements/store/rls-row-passes-policies.parity.test.ts +665 -0
- package/src/elements/store/schema-decl.ts +539 -41
- package/src/elements/store/sql-rls-stamp.test.ts +27 -0
- package/src/elements/store/sql-session.ts +297 -35
- package/src/elements/store/table.ts +102 -21
- package/src/elements/store.test.ts +3 -1
- package/src/elements/store.ts +12 -1
- package/src/elements/vault/chaos-child.ts +74 -1
- package/src/elements/vault/chaos.test.ts +4 -2
- package/src/elements/vault/storage.ts +4 -2
- package/src/index.ts +4 -1
- package/src/kernel/app-auth.ts +1 -0
- package/src/kernel/app.ts +116 -2
- package/src/kernel/auth-sharing.test.ts +196 -0
- package/src/kernel/boot.test.ts +3 -3
- package/src/kernel/errors.ts +8 -0
- package/src/kernel/fx.test.ts +1 -0
- package/src/kernel/fx.ts +14 -2
- package/src/kernel/horizontal-child.ts +2 -1
- package/src/kernel/http-resource.ts +33 -7
- package/src/kernel/identity-host-persist.test.ts +119 -0
- package/src/kernel/instance-id.ts +4 -2
- package/src/kernel/journal.ts +2 -1
- package/src/kernel/mcp-tool.test.ts +95 -0
- package/src/kernel/on.ts +9 -0
- package/src/kernel/realtime-bind.ts +326 -0
- package/src/kernel/resource-live.ts +117 -0
- package/src/kernel/triggers.ts +86 -4
- package/src/manifest/diff.ts +37 -0
- package/src/manifest/types.ts +64 -2
- package/src/okid.bench.test.ts +64 -0
- package/src/okid.test.ts +338 -0
- package/src/okid.ts +245 -0
- package/src/plugins/anonymous.ts +19 -1
- package/src/plugins/auth/shared.ts +15 -0
- package/src/plugins/index.ts +2 -0
- package/src/plugins/magic-link.ts +10 -8
- package/src/plugins/mcp-oauth.ts +208 -0
- package/src/plugins/oauth/flow-store.ts +117 -0
- package/src/plugins/oauth/link.ts +69 -0
- package/src/plugins/oauth/shared.ts +108 -0
- package/src/plugins/oauth/token-vault.ts +100 -0
- package/src/plugins/oauth.security.test.ts +535 -0
- package/src/plugins/oauth.ts +532 -0
- package/src/plugins/otp.ts +48 -6
- package/src/plugins/passkey.ts +20 -1
- package/src/plugins/two-factor.ts +11 -0
- package/src/plugins/username.ts +40 -7
- package/src/release/build-lib.ts +7 -1
- package/src/release/measure.ts +1 -0
- package/src/release/official-plugins.ts +4 -1
- package/src/runs/collect.ts +2 -1
- package/src/runs/drivers/files.ts +2 -1
- package/src/test/create-test-app.ts +114 -5
- package/src/test/export-bundle.test.ts +33 -0
- package/src/test/live-signals.test.ts +83 -0
- package/src/test/tenant-isolation.test.ts +175 -0
- package/src/testing.ts +26 -0
- package/src/upgrade/codemods.ts +1 -1
- package/site/content/docs/reference/migrating-environments.mdx +0 -158
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import { constantTimeEqual } from "../auth/constant-time.ts";
|
|
8
|
+
import { IdentityError, ensureUserExists } from "../auth/identity.ts";
|
|
8
9
|
import { issueSessionWithScopes } from "../auth/sessions.ts";
|
|
9
10
|
import { plugin, type PluginDef } from "../kernel/plugin.ts";
|
|
10
11
|
import {
|
|
@@ -16,6 +17,7 @@ import {
|
|
|
16
17
|
createMethodRuntime,
|
|
17
18
|
fail,
|
|
18
19
|
flow,
|
|
20
|
+
resolveSharedIdentities,
|
|
19
21
|
z,
|
|
20
22
|
type AuthMethodOptions,
|
|
21
23
|
} from "./auth/shared.ts";
|
|
@@ -56,6 +58,7 @@ export interface TwoFactorOptions extends AuthMethodOptions {
|
|
|
56
58
|
*/
|
|
57
59
|
export function twoFactor(opts: TwoFactorOptions = {}): PluginDef {
|
|
58
60
|
const runtime = createMethodRuntime(opts);
|
|
61
|
+
const identities = resolveSharedIdentities(opts);
|
|
59
62
|
const factors = opts.factors ?? createTwoFactorStore();
|
|
60
63
|
const issuer = opts.issuer ?? "oke";
|
|
61
64
|
|
|
@@ -70,6 +73,14 @@ export function twoFactor(opts: TwoFactorOptions = {}): PluginDef {
|
|
|
70
73
|
do: async (_input, fx) => {
|
|
71
74
|
const userId = fx.auth.userId;
|
|
72
75
|
if (!userId) return fail("AuthFailed", { reason: "unauthenticated" });
|
|
76
|
+
try {
|
|
77
|
+
await ensureUserExists(identities, userId, runtime.now());
|
|
78
|
+
} catch (err) {
|
|
79
|
+
if (err instanceof IdentityError) {
|
|
80
|
+
return fail("AuthFailed", { reason: "unauthenticated" });
|
|
81
|
+
}
|
|
82
|
+
throw err;
|
|
83
|
+
}
|
|
73
84
|
const secret = generateBase32Secret(20);
|
|
74
85
|
const recoveryCodes = Array.from({ length: 8 }, () => randomRecoveryCode());
|
|
75
86
|
const recoveryHashes = new Set<string>();
|
package/src/plugins/username.ts
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
import { assertNotBreached, BreachCheckError, type BreachCheckFn } from "../auth/breach-check.ts";
|
|
6
6
|
import { getActiveGateAuthContext } from "../auth/method-context.ts";
|
|
7
|
+
import { IdentityError, linkOrProvision } from "../auth/identity.ts";
|
|
7
8
|
import {
|
|
8
9
|
assertPasswordPolicy,
|
|
9
10
|
PasswordPolicyError,
|
|
@@ -21,6 +22,7 @@ import {
|
|
|
21
22
|
createMethodRuntime,
|
|
22
23
|
fail,
|
|
23
24
|
flow,
|
|
25
|
+
resolveSharedIdentities,
|
|
24
26
|
z,
|
|
25
27
|
type AuthMethodOptions,
|
|
26
28
|
} from "./auth/shared.ts";
|
|
@@ -311,6 +313,7 @@ export function username(opts: UsernamePluginOptions = {}): PluginDef {
|
|
|
311
313
|
const runtime = createMethodRuntime(opts);
|
|
312
314
|
const active = getActiveGateAuthContext();
|
|
313
315
|
const usernames = opts.usernames ?? createUsernameStore();
|
|
316
|
+
const identities = resolveSharedIdentities(opts);
|
|
314
317
|
const usernamePolicy = opts.usernamePolicy ?? {};
|
|
315
318
|
const passwordPolicy = opts.passwordPolicy ?? active?.passwordPolicy ?? {};
|
|
316
319
|
const passwordHash = opts.password ?? active?.password ?? { algorithm: "argon2id" };
|
|
@@ -359,18 +362,34 @@ export function username(opts: UsernamePluginOptions = {}): PluginDef {
|
|
|
359
362
|
if (usernames.byUsername.has(key)) {
|
|
360
363
|
return fail("AuthFailed", { reason: "invalid_credentials" });
|
|
361
364
|
}
|
|
362
|
-
const userId = crypto.randomUUID();
|
|
363
365
|
const passwordHashValue = await crypto.hashPassword(input.password, passwordHash);
|
|
366
|
+
let user: { id: string };
|
|
367
|
+
try {
|
|
368
|
+
user = (
|
|
369
|
+
await linkOrProvision(identities, {
|
|
370
|
+
provider: "username",
|
|
371
|
+
providerAccountId: key,
|
|
372
|
+
name: key,
|
|
373
|
+
passwordHash: passwordHashValue,
|
|
374
|
+
now: runtime.now,
|
|
375
|
+
})
|
|
376
|
+
).user;
|
|
377
|
+
} catch (err) {
|
|
378
|
+
if (err instanceof IdentityError) {
|
|
379
|
+
return fail("AuthFailed", { reason: "invalid_credentials" });
|
|
380
|
+
}
|
|
381
|
+
throw err;
|
|
382
|
+
}
|
|
364
383
|
const row: UsernameRow = {
|
|
365
|
-
userId,
|
|
384
|
+
userId: user.id,
|
|
366
385
|
username: key,
|
|
367
386
|
passwordHash: passwordHashValue,
|
|
368
387
|
createdAt: runtime.now(),
|
|
369
388
|
};
|
|
370
389
|
usernames.byUsername.set(key, row);
|
|
371
|
-
usernames.byUserId.set(
|
|
390
|
+
usernames.byUserId.set(user.id, row);
|
|
372
391
|
const issued = await issueSessionWithScopes(runtime.sessions, runtime.crypto, {
|
|
373
|
-
id:
|
|
392
|
+
id: user.id,
|
|
374
393
|
plane: "user",
|
|
375
394
|
scopes: [],
|
|
376
395
|
});
|
|
@@ -378,7 +397,7 @@ export function username(opts: UsernamePluginOptions = {}): PluginDef {
|
|
|
378
397
|
accessToken: issued.accessToken,
|
|
379
398
|
refreshToken: issued.refreshToken,
|
|
380
399
|
accessExpiresAt: issued.accessExpiresAt,
|
|
381
|
-
userId,
|
|
400
|
+
userId: user.id,
|
|
382
401
|
};
|
|
383
402
|
},
|
|
384
403
|
});
|
|
@@ -394,8 +413,22 @@ export function username(opts: UsernamePluginOptions = {}): PluginDef {
|
|
|
394
413
|
const hash = row?.passwordHash ?? (await dummyHash(crypto));
|
|
395
414
|
const ok = await crypto.verifyPassword(input.password, hash);
|
|
396
415
|
if (!ok || !row) return fail("AuthFailed", { reason: "invalid_credentials" });
|
|
416
|
+
const userId = row.userId;
|
|
417
|
+
try {
|
|
418
|
+
await linkOrProvision(identities, {
|
|
419
|
+
provider: "username",
|
|
420
|
+
providerAccountId: key,
|
|
421
|
+
currentUserId: userId,
|
|
422
|
+
now: runtime.now,
|
|
423
|
+
});
|
|
424
|
+
} catch (err) {
|
|
425
|
+
if (err instanceof IdentityError) {
|
|
426
|
+
return fail("AuthFailed", { reason: "invalid_credentials" });
|
|
427
|
+
}
|
|
428
|
+
throw err;
|
|
429
|
+
}
|
|
397
430
|
const issued = await issueSessionWithScopes(runtime.sessions, runtime.crypto, {
|
|
398
|
-
id:
|
|
431
|
+
id: userId,
|
|
399
432
|
plane: "user",
|
|
400
433
|
scopes: [],
|
|
401
434
|
});
|
|
@@ -403,7 +436,7 @@ export function username(opts: UsernamePluginOptions = {}): PluginDef {
|
|
|
403
436
|
accessToken: issued.accessToken,
|
|
404
437
|
refreshToken: issued.refreshToken,
|
|
405
438
|
accessExpiresAt: issued.accessExpiresAt,
|
|
406
|
-
userId
|
|
439
|
+
userId,
|
|
407
440
|
};
|
|
408
441
|
},
|
|
409
442
|
});
|
package/src/release/build-lib.ts
CHANGED
|
@@ -20,6 +20,7 @@ const ENTRIES: readonly { readonly src: string; readonly out: string }[] = [
|
|
|
20
20
|
{ src: "src/kernel-entry.ts", out: "dist/kernel-entry.js" },
|
|
21
21
|
{ src: "src/full.ts", out: "dist/full.js" },
|
|
22
22
|
{ src: "src/i18n-entry.ts", out: "dist/i18n-entry.js" },
|
|
23
|
+
{ src: "src/testing.ts", out: "dist/testing.js" },
|
|
23
24
|
{ src: "src/compiler-entry.ts", out: "dist/compiler-entry.js" },
|
|
24
25
|
{ src: "src/journal-entry.ts", out: "dist/journal-entry.js" },
|
|
25
26
|
// Lazy sync chunks — loaded via requirePackageModule / computed require
|
|
@@ -44,6 +45,7 @@ const ENTRIES: readonly { readonly src: string; readonly out: string }[] = [
|
|
|
44
45
|
{ src: "src/elements/store/cache.ts", out: "dist/store-cache.js" },
|
|
45
46
|
{ src: "src/i18n/messages.ts", out: "dist/messages.js" },
|
|
46
47
|
{ src: "src/i18n/failure-message.ts", out: "dist/failure-message.js" },
|
|
48
|
+
{ src: "src/okid.ts", out: "dist/okid.js" },
|
|
47
49
|
];
|
|
48
50
|
|
|
49
51
|
/**
|
|
@@ -63,7 +65,11 @@ export async function buildLib(): Promise<void> {
|
|
|
63
65
|
naming: relative(DIST, absOut).replace(/\\/g, "/"),
|
|
64
66
|
target: "bun",
|
|
65
67
|
format: "esm",
|
|
66
|
-
minify
|
|
68
|
+
// Non-Bun consumers ship this output; minify keeps `dist/` compact.
|
|
69
|
+
// Budgets measure a *separate* minified build (measure.ts), but the
|
|
70
|
+
// published artifact should not ship full-size sources when the compiler
|
|
71
|
+
// is available to minify at the same cost.
|
|
72
|
+
minify: true,
|
|
67
73
|
sourcemap: "external",
|
|
68
74
|
external: [...EXPORT_BUILD_EXTERNALS],
|
|
69
75
|
splitting: false,
|
package/src/release/measure.ts
CHANGED
|
@@ -66,6 +66,7 @@ const GROUP_HEADINGS: Readonly<Record<BudgetGroup, string>> = {
|
|
|
66
66
|
/** Docs category → markdown subheading under Plugins. */
|
|
67
67
|
const PLUGIN_CATEGORY_HEADINGS: Readonly<Record<string, string>> = {
|
|
68
68
|
auth: "Auth",
|
|
69
|
+
oauth: "OAuth",
|
|
69
70
|
security: "Security",
|
|
70
71
|
ops: "Ops",
|
|
71
72
|
perf: "Perf",
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
/** Docs / landing category for an official plugin. */
|
|
8
|
-
export type PluginBudgetCategory = "auth" | "security" | "ops" | "perf";
|
|
8
|
+
export type PluginBudgetCategory = "auth" | "oauth" | "security" | "ops" | "perf";
|
|
9
9
|
|
|
10
10
|
/** One official plugin entry measured as `export:./plugins/<name>`. */
|
|
11
11
|
export type OfficialPluginBudget = {
|
|
@@ -20,6 +20,7 @@ export type OfficialPluginBudget = {
|
|
|
20
20
|
/** Category display order (docs index). */
|
|
21
21
|
export const PLUGIN_BUDGET_CATEGORIES: readonly PluginBudgetCategory[] = [
|
|
22
22
|
"auth",
|
|
23
|
+
"oauth",
|
|
23
24
|
"security",
|
|
24
25
|
"ops",
|
|
25
26
|
"perf",
|
|
@@ -36,6 +37,8 @@ export const OFFICIAL_PLUGIN_BUDGETS: readonly OfficialPluginBudget[] = [
|
|
|
36
37
|
{ name: "otp", file: "otp.ts", category: "auth" },
|
|
37
38
|
{ name: "twoFactor", file: "two-factor.ts", category: "auth" },
|
|
38
39
|
{ name: "passkey", file: "passkey.ts", category: "auth" },
|
|
40
|
+
{ name: "oauth", file: "oauth.ts", category: "oauth" },
|
|
41
|
+
{ name: "mcpOauth", file: "mcp-oauth.ts", category: "oauth" },
|
|
39
42
|
{ name: "headers", file: "headers.ts", category: "security" },
|
|
40
43
|
{ name: "cors", file: "cors.ts", category: "security" },
|
|
41
44
|
{ name: "csrf", file: "csrf.ts", category: "security" },
|
package/src/runs/collect.ts
CHANGED
|
@@ -12,6 +12,7 @@ import type { Fx } from "../kernel/fx.ts";
|
|
|
12
12
|
import type { Trigger } from "../kernel/triggers.ts";
|
|
13
13
|
import type { FlowPlane } from "../manifest/types.ts";
|
|
14
14
|
import { cacheDimensionOf, type RunTelemetry } from "../kernel/run-telemetry.ts";
|
|
15
|
+
import { okid } from "../okid.ts";
|
|
15
16
|
import type { RunError, WideEvent } from "./types.ts";
|
|
16
17
|
|
|
17
18
|
/** Inputs for {@link collectWideEvent}. */
|
|
@@ -97,7 +98,7 @@ export function collectWideEvent(input: CollectWideEventInput): WideEvent {
|
|
|
97
98
|
};
|
|
98
99
|
|
|
99
100
|
return {
|
|
100
|
-
id: input.id ??
|
|
101
|
+
id: input.id ?? okid(),
|
|
101
102
|
...(input.parentId !== undefined ? { parentId: input.parentId } : {}),
|
|
102
103
|
flow: input.flow.name,
|
|
103
104
|
...(input.flow.unit !== undefined ? { unit: input.flow.unit } : {}),
|
|
@@ -12,6 +12,7 @@ import { join, resolve } from "node:path";
|
|
|
12
12
|
|
|
13
13
|
import { fsDriver } from "../../drivers/fs.ts";
|
|
14
14
|
import type { FilesBucket } from "../../drivers/types.ts";
|
|
15
|
+
import { okid } from "../../okid.ts";
|
|
15
16
|
import {
|
|
16
17
|
duckPath,
|
|
17
18
|
duckQuery,
|
|
@@ -102,7 +103,7 @@ export const filesRunsDriver: RunsDriver = {
|
|
|
102
103
|
locality === "remote" && remote
|
|
103
104
|
? { bucket: remote, keyPrefix: remotePrefix }
|
|
104
105
|
: { bucket: local, keyPrefix: "" };
|
|
105
|
-
const fileId =
|
|
106
|
+
const fileId = okid();
|
|
106
107
|
const objectKey = `${target.keyPrefix}${partitionObjectKey(day!, fileId)}`;
|
|
107
108
|
const tmpDir = await mkdtemp(join(tmpdir(), "oke-pq-"));
|
|
108
109
|
const tmp = join(tmpDir, `${fileId}.parquet`);
|
|
@@ -44,6 +44,8 @@ export interface TestUser {
|
|
|
44
44
|
readonly id: string;
|
|
45
45
|
readonly scopes: ReadonlySet<string>;
|
|
46
46
|
readonly verified: boolean;
|
|
47
|
+
/** Tenant id stamped onto `fx.tenant.id` for scoped calls. */
|
|
48
|
+
readonly tenantId?: string;
|
|
47
49
|
}
|
|
48
50
|
|
|
49
51
|
/** Options for {@link TestAuth.loginAs}. */
|
|
@@ -52,11 +54,15 @@ export interface LoginAsOptions {
|
|
|
52
54
|
readonly scopes?: readonly string[];
|
|
53
55
|
readonly verified?: boolean;
|
|
54
56
|
readonly plane?: "user" | "operator";
|
|
57
|
+
/** Tenant the principal belongs to — stamped onto `fx.tenant.id`. */
|
|
58
|
+
readonly tenantId?: string;
|
|
55
59
|
}
|
|
56
60
|
|
|
57
61
|
/** Second argument to test API calls. */
|
|
58
62
|
export interface TestCallOptions {
|
|
59
63
|
readonly as?: TestUser | ResolvedPrincipal;
|
|
64
|
+
/** Override the tenant stamped for this call (overrides `as.tenantId`). */
|
|
65
|
+
readonly tenant?: string;
|
|
60
66
|
}
|
|
61
67
|
|
|
62
68
|
/** Auth surface on the harness. */
|
|
@@ -91,10 +97,44 @@ export interface TestCron {
|
|
|
91
97
|
run(name: string): Promise<boolean>;
|
|
92
98
|
}
|
|
93
99
|
|
|
100
|
+
/** Live-subscription handlers for {@link TestSignals.subscribeLive}. */
|
|
101
|
+
export interface TestLiveHandlers<T = unknown> {
|
|
102
|
+
/** Called with each live event payload. */
|
|
103
|
+
readonly onEvent: (event: T) => void;
|
|
104
|
+
/** Called when the live stream errors. */
|
|
105
|
+
readonly onError?: (error: unknown) => void;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** Unsubscribe handle returned by {@link TestSignals.subscribeLive}. */
|
|
109
|
+
export type TestLiveUnsubscribe = () => void;
|
|
110
|
+
|
|
94
111
|
/** Signal surface. */
|
|
95
112
|
export interface TestSignals {
|
|
96
113
|
/** Drain the bus until idle (deterministic queued work). */
|
|
97
114
|
drain(): Promise<void>;
|
|
115
|
+
/**
|
|
116
|
+
* Subscribe to a `delivery: "live"` signal's event stream.
|
|
117
|
+
*
|
|
118
|
+
* @param signal - Signal declaration or name
|
|
119
|
+
* @param handlers - Event / error handlers
|
|
120
|
+
* @returns Unsubscribe function
|
|
121
|
+
*/
|
|
122
|
+
subscribeLive<T = unknown>(
|
|
123
|
+
signal: SignalDecl | string,
|
|
124
|
+
handlers: TestLiveHandlers<T>,
|
|
125
|
+
): TestLiveUnsubscribe;
|
|
126
|
+
/**
|
|
127
|
+
* Resolve once a live event matching `predicate` arrives.
|
|
128
|
+
*
|
|
129
|
+
* @param signal - Signal declaration or name
|
|
130
|
+
* @param predicate - Event filter
|
|
131
|
+
* @param timeoutMs - Reject after this many ms (default 1000)
|
|
132
|
+
*/
|
|
133
|
+
waitForLive<T = unknown>(
|
|
134
|
+
signal: SignalDecl | string,
|
|
135
|
+
predicate: (event: T) => boolean,
|
|
136
|
+
timeoutMs?: number,
|
|
137
|
+
): Promise<T>;
|
|
98
138
|
}
|
|
99
139
|
|
|
100
140
|
/** Channel surface. */
|
|
@@ -269,6 +309,7 @@ export async function createTestApp<App extends OkeApp>(
|
|
|
269
309
|
id: opts.id ?? `user_${crypto.randomUUID().slice(0, 8)}`,
|
|
270
310
|
scopes: new Set(opts.scopes ?? []),
|
|
271
311
|
verified: opts.verified ?? true,
|
|
312
|
+
...(opts.tenantId !== undefined ? { tenantId: opts.tenantId } : {}),
|
|
272
313
|
};
|
|
273
314
|
},
|
|
274
315
|
};
|
|
@@ -306,6 +347,63 @@ export async function createTestApp<App extends OkeApp>(
|
|
|
306
347
|
const bus = app.bootResult?.signal?.bus;
|
|
307
348
|
if (bus) await bus.drain();
|
|
308
349
|
},
|
|
350
|
+
subscribeLive(signal, handlers) {
|
|
351
|
+
const name = typeof signal === "string" ? signal : signal.name;
|
|
352
|
+
let active = true;
|
|
353
|
+
const rt = app.bootResult?.signal;
|
|
354
|
+
void (async () => {
|
|
355
|
+
const iter = rt?.live(name)[Symbol.asyncIterator]();
|
|
356
|
+
if (!iter) return;
|
|
357
|
+
for (;;) {
|
|
358
|
+
if (!active) {
|
|
359
|
+
await iter.return?.();
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
362
|
+
const { done, value } = await iter.next();
|
|
363
|
+
if (done || !active) {
|
|
364
|
+
await iter.return?.();
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
if (!active) break;
|
|
368
|
+
try {
|
|
369
|
+
handlers.onEvent(value.payload as never);
|
|
370
|
+
} catch (err) {
|
|
371
|
+
handlers.onError?.(err);
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
})();
|
|
375
|
+
return () => {
|
|
376
|
+
active = false;
|
|
377
|
+
};
|
|
378
|
+
},
|
|
379
|
+
waitForLive(signal, predicate, timeoutMs) {
|
|
380
|
+
const name = typeof signal === "string" ? signal : signal.name;
|
|
381
|
+
return new Promise((resolve, reject) => {
|
|
382
|
+
let unsubscribe: TestLiveUnsubscribe | undefined;
|
|
383
|
+
const timer = setTimeout(() => {
|
|
384
|
+
unsubscribe?.();
|
|
385
|
+
reject(
|
|
386
|
+
new Error(
|
|
387
|
+
`waitForLive: no matching event for "${name}" within ${timeoutMs ?? 1000}ms`,
|
|
388
|
+
),
|
|
389
|
+
);
|
|
390
|
+
}, timeoutMs ?? 1000);
|
|
391
|
+
unsubscribe = this.subscribeLive(signal as never, {
|
|
392
|
+
onEvent: (event: unknown) => {
|
|
393
|
+
if (predicate(event as never)) {
|
|
394
|
+
clearTimeout(timer);
|
|
395
|
+
unsubscribe?.();
|
|
396
|
+
resolve(event as never);
|
|
397
|
+
}
|
|
398
|
+
},
|
|
399
|
+
onError: (err) => {
|
|
400
|
+
clearTimeout(timer);
|
|
401
|
+
unsubscribe?.();
|
|
402
|
+
reject(err);
|
|
403
|
+
},
|
|
404
|
+
});
|
|
405
|
+
});
|
|
406
|
+
},
|
|
309
407
|
},
|
|
310
408
|
channels: {
|
|
311
409
|
sent() {
|
|
@@ -340,7 +438,10 @@ export async function createTestApp<App extends OkeApp>(
|
|
|
340
438
|
return events;
|
|
341
439
|
},
|
|
342
440
|
async close() {
|
|
343
|
-
|
|
441
|
+
// Prefer `app.stop()` so the process-wide realtime bridge unbinds with
|
|
442
|
+
// the SQL connections — bare `bootResult.close()` left CDC classifying
|
|
443
|
+
// against a dead PGLite instance for later tests in the same process.
|
|
444
|
+
await app.stop();
|
|
344
445
|
},
|
|
345
446
|
};
|
|
346
447
|
|
|
@@ -381,7 +482,7 @@ function createTestApi(app: OkeApp, now: () => number): TestApi {
|
|
|
381
482
|
};
|
|
382
483
|
}
|
|
383
484
|
|
|
384
|
-
const principal = toPrincipal(opts?.as);
|
|
485
|
+
const principal = toPrincipal(opts?.as, opts?.tenant);
|
|
385
486
|
const httpTrigger = flowDef.triggers.find((t) => t.kind === "http");
|
|
386
487
|
const trigger: Trigger =
|
|
387
488
|
httpTrigger ?? flowDef.triggers[0] ?? ({ kind: "internal" } satisfies InternalTrigger);
|
|
@@ -389,6 +490,7 @@ function createTestApi(app: OkeApp, now: () => number): TestApi {
|
|
|
389
490
|
const result = await app.execute(flowDef, input, trigger, {
|
|
390
491
|
principal,
|
|
391
492
|
auth: principal,
|
|
493
|
+
...(principal?.tenantId !== undefined ? { tenant: { id: principal.tenantId } } : {}),
|
|
392
494
|
});
|
|
393
495
|
|
|
394
496
|
// Keep effects cache warm for t.effects.of after the caller awaits runs().
|
|
@@ -533,8 +635,13 @@ function matchRestHeuristic(
|
|
|
533
635
|
return undefined;
|
|
534
636
|
}
|
|
535
637
|
|
|
536
|
-
function toPrincipal(
|
|
537
|
-
|
|
638
|
+
function toPrincipal(
|
|
639
|
+
as: TestUser | ResolvedPrincipal | undefined,
|
|
640
|
+
tenantOverride?: string,
|
|
641
|
+
): ResolvedPrincipal | undefined {
|
|
642
|
+
if (!as) {
|
|
643
|
+
return tenantOverride ? { tenantId: tenantOverride } : undefined;
|
|
644
|
+
}
|
|
538
645
|
if ("scopes" in as && as.scopes instanceof Set && "id" in as) {
|
|
539
646
|
const u = as as TestUser;
|
|
540
647
|
return {
|
|
@@ -542,9 +649,11 @@ function toPrincipal(as: TestUser | ResolvedPrincipal | undefined): ResolvedPrin
|
|
|
542
649
|
userId: u.id,
|
|
543
650
|
scopes: u.scopes,
|
|
544
651
|
verified: u.verified,
|
|
652
|
+
tenantId: tenantOverride ?? u.tenantId,
|
|
545
653
|
};
|
|
546
654
|
}
|
|
547
|
-
|
|
655
|
+
const p = as as ResolvedPrincipal;
|
|
656
|
+
return tenantOverride ? { ...p, tenantId: tenantOverride } : p;
|
|
548
657
|
}
|
|
549
658
|
|
|
550
659
|
/** @internal re-export fail for harness tests that assert typed denials */
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `okengine/testing` subpath export & core/client bundle isolation.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, expect, test } from "bun:test";
|
|
6
|
+
import { readFileSync } from "node:fs";
|
|
7
|
+
import { resolve } from "node:path";
|
|
8
|
+
import { measureClientGzipBytes, measureKernelEdgeGzipBytes } from "../release/measure.ts";
|
|
9
|
+
import { CLIENT_BUDGET_BYTES, KERNEL_EDGE_BUDGET_BYTES } from "../release/limits.ts";
|
|
10
|
+
|
|
11
|
+
describe("testing entrypoint export & bundle isolation", () => {
|
|
12
|
+
test("package.json exports ./testing and ./test point to the harness entry", () => {
|
|
13
|
+
const pkg = JSON.parse(
|
|
14
|
+
readFileSync(resolve(import.meta.dir, "../../package.json"), "utf8"),
|
|
15
|
+
) as {
|
|
16
|
+
exports: Record<string, unknown>;
|
|
17
|
+
};
|
|
18
|
+
const testing = pkg.exports["./testing"] as Record<string, string>;
|
|
19
|
+
expect(testing).toBeDefined();
|
|
20
|
+
expect(testing.bun).toBe("./src/testing.ts");
|
|
21
|
+
expect(testing.types).toBe("./src/testing.ts");
|
|
22
|
+
expect(testing.import).toBe("./dist/testing.js");
|
|
23
|
+
expect(testing.default).toBe("./dist/testing.js");
|
|
24
|
+
expect(pkg.exports["./test"]).toBeDefined();
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test("importing okengine or okengine/http does not pull the test harness into core/client bundles", async () => {
|
|
28
|
+
const edgeSize = await measureKernelEdgeGzipBytes();
|
|
29
|
+
const clientSize = await measureClientGzipBytes();
|
|
30
|
+
expect(edgeSize).toBeLessThan(KERNEL_EDGE_BUDGET_BYTES);
|
|
31
|
+
expect(clientSize).toBeLessThan(CLIENT_BUDGET_BYTES);
|
|
32
|
+
});
|
|
33
|
+
});
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `createTestApp` deterministic live-signal subscriptions
|
|
3
|
+
* (`signals.subscribeLive` / `signals.waitForLive`).
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { describe, expect, test } from "bun:test";
|
|
7
|
+
import { z } from "zod";
|
|
8
|
+
import { oke } from "../kernel/app.ts";
|
|
9
|
+
import { flow, resetFlowSeq } from "../kernel/flow.ts";
|
|
10
|
+
import { on, resetBindings } from "../kernel/on.ts";
|
|
11
|
+
import { http } from "../kernel/triggers.ts";
|
|
12
|
+
import { signal } from "../elements/signal.ts";
|
|
13
|
+
import { createTestApp } from "./create-test-app.ts";
|
|
14
|
+
|
|
15
|
+
describe("createTestApp live-signal subscriptions", () => {
|
|
16
|
+
test("subscribeLive observes deterministic live emits; waitForLive resolves matching payloads", async () => {
|
|
17
|
+
resetBindings();
|
|
18
|
+
resetFlowSeq();
|
|
19
|
+
|
|
20
|
+
const orderStatus = signal("order-status", {
|
|
21
|
+
delivery: "live",
|
|
22
|
+
retention: { maxCount: 10 },
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
on(
|
|
26
|
+
http.post("/orders/status").public(),
|
|
27
|
+
flow("orders.updateStatus", {
|
|
28
|
+
in: z.object({ orderId: z.string(), status: z.string() }),
|
|
29
|
+
effects: { emits: ["order-status"] },
|
|
30
|
+
do: async (input, fx) => {
|
|
31
|
+
await fx.emit(orderStatus, input);
|
|
32
|
+
return { ok: true };
|
|
33
|
+
},
|
|
34
|
+
}),
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
const app = oke({
|
|
38
|
+
name: "live-signals-app",
|
|
39
|
+
signals: [orderStatus],
|
|
40
|
+
env: "test",
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
const t = await createTestApp(app, {
|
|
44
|
+
signals: [orderStatus],
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
const seen: Array<{ orderId: string; status: string }> = [];
|
|
48
|
+
const unsubscribe = t.signals.subscribeLive(orderStatus, {
|
|
49
|
+
onEvent: (event) => {
|
|
50
|
+
seen.push(event as { orderId: string; status: string });
|
|
51
|
+
},
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
// Emit 1 — deterministic after drain.
|
|
55
|
+
const res1 = await t.api.orders!.updateStatus!({ orderId: "o1", status: "placed" });
|
|
56
|
+
expect(res1.error).toBeNull();
|
|
57
|
+
await t.signals.drain();
|
|
58
|
+
expect(seen.map((e) => e.status)).toEqual(["placed"]);
|
|
59
|
+
|
|
60
|
+
// Emit 2 — observer updates.
|
|
61
|
+
await t.api.orders!.updateStatus!({ orderId: "o1", status: "shipped" });
|
|
62
|
+
await t.signals.drain();
|
|
63
|
+
expect(seen.map((e) => e.status)).toEqual(["placed", "shipped"]);
|
|
64
|
+
|
|
65
|
+
// waitForLive resolves the first payload matching the predicate.
|
|
66
|
+
const deliveredPromise = t.signals.waitForLive(
|
|
67
|
+
orderStatus,
|
|
68
|
+
(event) => (event as { status: string }).status === "delivered",
|
|
69
|
+
);
|
|
70
|
+
await t.api.orders!.updateStatus!({ orderId: "o1", status: "delivered" });
|
|
71
|
+
await t.signals.drain();
|
|
72
|
+
const delivered = await deliveredPromise;
|
|
73
|
+
expect((delivered as { status: string }).status).toBe("delivered");
|
|
74
|
+
|
|
75
|
+
// Unsubscribe then emit — the subscription must not observe it.
|
|
76
|
+
unsubscribe();
|
|
77
|
+
await t.api.orders!.updateStatus!({ orderId: "o2", status: "placed" });
|
|
78
|
+
await t.signals.drain();
|
|
79
|
+
expect(seen).toHaveLength(3);
|
|
80
|
+
|
|
81
|
+
await t.close();
|
|
82
|
+
});
|
|
83
|
+
});
|