okengine 0.11.0 → 0.11.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "okengine",
3
- "version": "0.11.0",
3
+ "version": "0.11.1",
4
4
  "description": "One law. Eight elements. Ten exports. One package. One manifest. Every backend need is derived, never added.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -1,8 +1,12 @@
1
1
  /**
2
2
  * `oke({ gate })` — nested Gate bag (auth · policies · rate · posture).
3
+ *
4
+ * Auth resolution is sync-lazy via {@link requirePackageModule} so HTTP-only
5
+ * apps never evaluate `auth/config` (and so `dist/` can ship a separate chunk).
3
6
  */
4
7
 
5
- import { resolveGateAuth, type GateAuthOptions, type ResolvedGateAuth } from "../../auth/config.ts";
8
+ import type { GateAuthOptions, ResolvedGateAuth } from "../../auth/config.ts";
9
+ import { requirePackageModule } from "../../shared/lazy-src.ts";
6
10
  import type { GateDecl } from "./declare.ts";
7
11
 
8
12
  /** Rate-limit defaults under Gate. */
@@ -51,8 +55,14 @@ export interface ResolveGateConfigOptions {
51
55
  */
52
56
  export function resolveGateConfig(options: ResolveGateConfigOptions = {}): ResolvedGateConfig {
53
57
  const bag = options.gate ?? {};
54
- const auth =
55
- bag.auth !== undefined ? resolveGateAuth({ auth: bag.auth, env: options.env }) : undefined;
58
+ let auth: ResolvedGateAuth | undefined;
59
+ if (bag.auth !== undefined) {
60
+ const { resolveGateAuth } = requirePackageModule<typeof import("../../auth/config.ts")>(
61
+ "auth/config",
62
+ "auth-config",
63
+ );
64
+ auth = resolveGateAuth({ auth: bag.auth, env: options.env });
65
+ }
56
66
  const rateLimitEnabled =
57
67
  bag.rateLimit?.enabled !== undefined
58
68
  ? bag.rateLimit.enabled
@@ -5,7 +5,7 @@
5
5
  */
6
6
 
7
7
  import type { RateStrategy } from "../../manifest/types.ts";
8
- import { DEFAULT_RATE_STRATEGY } from "./strategies.ts";
8
+ import { DEFAULT_RATE_STRATEGY } from "./constants.ts";
9
9
 
10
10
  /** Context passed to policy predicates at evaluation time. */
11
11
  export interface GatePolicyContext {
@@ -11,6 +11,8 @@
11
11
  import { registerLuaScript, type LuaKvStore } from "../../drivers/kv-lua.ts";
12
12
  import type { RateStrategy } from "../../manifest/types.ts";
13
13
 
14
+ export { ALL_RATE_STRATEGIES, DEFAULT_RATE_STRATEGY } from "./constants.ts";
15
+
14
16
  /** Result of a rate-limit take attempt. */
15
17
  export interface RateTakeResult {
16
18
  /** Whether the request is allowed. */
@@ -338,18 +340,6 @@ export const RATE_STRATEGIES: Record<RateStrategy, StrategyDef> = {
338
340
  "leaky-bucket": leakyBucket,
339
341
  };
340
342
 
341
- /** Default rate strategy (unified-theory §16). */
342
- export const DEFAULT_RATE_STRATEGY: RateStrategy = "sliding-window-counter";
343
-
344
- /** All five strategy ids. */
345
- export const ALL_RATE_STRATEGIES: readonly RateStrategy[] = [
346
- "fixed-window",
347
- "sliding-window-counter",
348
- "sliding-log",
349
- "token-bucket",
350
- "leaky-bucket",
351
- ];
352
-
353
343
  for (const def of Object.values(RATE_STRATEGIES)) {
354
344
  registerLuaScript(def.lua, (store, keys, args) => def.run(store, keys, args));
355
345
  }
@@ -0,0 +1,98 @@
1
+ /**
2
+ * Sync auth wiring for `oke({ gate: { auth } })`.
3
+ *
4
+ * Loaded only when auth is configured so HTTP-only cold graphs never import
5
+ * bindings / sessions / identity (`requirePackageModule` in `app.ts`).
6
+ */
7
+
8
+ import type { ResolvedGateAuth } from "../auth/config.ts";
9
+ import { auth as authPlugin } from "../auth/plugin.ts";
10
+ import { createAuthHttpBindings, type AuthHttpMaterialization } from "../auth/bindings.ts";
11
+ import { tokenFromCookieHeader } from "../auth/cookies.ts";
12
+ import { setActiveGateAuthContext } from "../auth/method-context.ts";
13
+ import type { ResolvedGateConfig } from "../elements/gate/config.ts";
14
+ import { createAppAuthBinding, verifyBearerToken, type AppAuthBinding } from "./auth-resolve.ts";
15
+ import type { PluginDef } from "./plugin.ts";
16
+
17
+ /** Result of wiring Gate auth at construction. */
18
+ export interface WiredGateAuth {
19
+ readonly materialization: AuthHttpMaterialization | undefined;
20
+ readonly authPlugin: PluginDef | undefined;
21
+ readonly authBinding: AppAuthBinding;
22
+ readonly verifyBearerToken: typeof verifyBearerToken;
23
+ readonly tokenFromCookieHeader: typeof tokenFromCookieHeader;
24
+ /** Rebuild binding when boot supplies a clock. */
25
+ rebind(now: () => number): AppAuthBinding;
26
+ }
27
+
28
+ /** Options for {@link wireGateAuth}. */
29
+ export interface WireGateAuthOptions {
30
+ readonly gateConfig: ResolvedGateConfig & { readonly auth: ResolvedGateAuth };
31
+ /** Clock fallback when `gate.auth.now` is omitted (`oke({ fx: { now } })`). */
32
+ readonly now?: () => number;
33
+ }
34
+
35
+ /**
36
+ * Materialize auth HTTP bindings, absorb the builtin auth plugin, and publish
37
+ * the active Gate auth context for `.plug(username())` etc.
38
+ *
39
+ * @param options - Resolved Gate bag + optional clock
40
+ */
41
+ export function wireGateAuth(options: WireGateAuthOptions): WiredGateAuth {
42
+ const { gateConfig } = options;
43
+ const auth = gateConfig.auth;
44
+ let materialization: AuthHttpMaterialization | undefined;
45
+ if (auth.http) {
46
+ materialization = createAuthHttpBindings(auth, {
47
+ rateLimitEnabled: gateConfig.rateLimitEnabled,
48
+ sessions: auth.sessions,
49
+ });
50
+ }
51
+
52
+ const pluginDef = authPlugin({
53
+ secret: auth.secret,
54
+ accessTtlMs: auth.session.accessTtlMs,
55
+ refreshTtlMs: auth.session.refreshTtlMs,
56
+ session: {
57
+ accessTtlMs: auth.session.accessTtlMs,
58
+ refreshTtlMs: auth.session.refreshTtlMs,
59
+ idleTtlMs: auth.session.idleTtlMs,
60
+ absoluteTtlMs: auth.session.absoluteTtlMs,
61
+ singleSessionPerUser: auth.session.singleSessionPerUser,
62
+ },
63
+ password: auth.password,
64
+ passwordPolicy: auth.passwordPolicy,
65
+ breachCheck: auth.breachCheck,
66
+ });
67
+
68
+ let authBinding = createAppAuthBinding({
69
+ secret: auth.secret,
70
+ sessions: materialization?.ctx.sessions ?? auth.sessions,
71
+ now: auth.now ?? options.now,
72
+ });
73
+
74
+ setActiveGateAuthContext({
75
+ secret: authBinding.secret,
76
+ sessions: authBinding.sessions,
77
+ now: authBinding.now,
78
+ passwordPolicy: auth.passwordPolicy,
79
+ password: auth.password,
80
+ breachCheck: auth.breachCheck,
81
+ });
82
+
83
+ return {
84
+ materialization,
85
+ authPlugin: pluginDef,
86
+ authBinding,
87
+ verifyBearerToken,
88
+ tokenFromCookieHeader,
89
+ rebind(now) {
90
+ authBinding = createAppAuthBinding({
91
+ secret: authBinding.secret,
92
+ sessions: authBinding.sessions,
93
+ now,
94
+ });
95
+ return authBinding;
96
+ },
97
+ };
98
+ }
package/src/kernel/app.ts CHANGED
@@ -6,12 +6,9 @@
6
6
  * pipeline, and wires `fx.call` to untriggered (and triggered) flows.
7
7
  */
8
8
 
9
- import {
10
- compileRoute,
11
- encodeExecuteResult,
12
- encodeFailure,
13
- type CompiledRoute,
14
- } from "../compiler/index.ts";
9
+ import { compileRoute } from "../compiler/dynamic.ts";
10
+ import type { CompiledRoute } from "../compiler/aot.ts";
11
+ import { encodeExecuteResult, encodeFailure } from "../compiler/response.ts";
15
12
  import { validate } from "../validation/standard-schema.ts";
16
13
  import {
17
14
  accumulateAdoptArgs,
@@ -25,16 +22,15 @@ import {
25
22
  // when an app actually boots (AGENTS.md / unified-theory budget: cold start < 75 ms).
26
23
  import type { BootOptions, BootResult, ElementRuntimes } from "./boot.ts";
27
24
  import type { CapabilityToken } from "./capability.ts";
28
- import { createAppAuthBinding, verifyBearerToken, type AppAuthBinding } from "./auth-resolve.ts";
29
- import { createAuthHttpBindings, type AuthHttpMaterialization } from "../auth/bindings.ts";
30
- import { auth as authPlugin } from "../auth/plugin.ts";
31
- import { tokenFromCookieHeader } from "../auth/cookies.ts";
32
- import { setActiveGateAuthContext } from "../auth/method-context.ts";
25
+ import type { AppAuthBinding } from "./auth-resolve.ts";
26
+ import type { AuthHttpMaterialization } from "../auth/bindings.ts";
27
+ import type { WiredGateAuth } from "./app-auth.ts";
33
28
  import {
34
29
  resolveGateConfig,
35
30
  type GateOptions,
36
31
  type ResolvedGateConfig,
37
32
  } from "../elements/gate/config.ts";
33
+ import { requirePackageModule } from "../shared/lazy-src.ts";
38
34
  import type { TemplateCatalog } from "../elements/channel/runtime.ts";
39
35
  import { parseAcceptLanguage } from "../elements/channel/locale.ts";
40
36
  import { runWithLocale } from "../i18n/locale-context.ts";
@@ -534,14 +530,6 @@ export function oke(options: OkeOptions): OkeApp {
534
530
  gate: options.gate,
535
531
  env: options.env,
536
532
  });
537
- let authMaterialization: AuthHttpMaterialization | undefined;
538
- if (gateConfig.auth?.http) {
539
- authMaterialization = createAuthHttpBindings(gateConfig.auth, {
540
- rateLimitEnabled: gateConfig.rateLimitEnabled,
541
- sessions: gateConfig.auth.sessions,
542
- });
543
- adopted.push(...authMaterialization.bindings);
544
- }
545
533
 
546
534
  /** Retained for test harness / boot merges. */
547
535
  const $options = options;
@@ -555,27 +543,26 @@ export function oke(options: OkeOptions): OkeApp {
555
543
  /** Runtime route table — types accumulate on the returned {@link OkeApp}. */
556
544
  const routes: RuntimeRouteMap = {};
557
545
 
558
- // Absorb auth() tables into gate.auth — `.needs("auth")` sees plugin name.
546
+ let authMaterialization: AuthHttpMaterialization | undefined;
547
+ let wiredAuth: WiredGateAuth | undefined;
548
+ let authBinding: AppAuthBinding | undefined;
559
549
  if (gateConfig.auth) {
560
- applyPlugin(
561
- pluginRegistry,
562
- authPlugin({
563
- secret: gateConfig.auth.secret,
564
- accessTtlMs: gateConfig.auth.session.accessTtlMs,
565
- refreshTtlMs: gateConfig.auth.session.refreshTtlMs,
566
- session: {
567
- accessTtlMs: gateConfig.auth.session.accessTtlMs,
568
- refreshTtlMs: gateConfig.auth.session.refreshTtlMs,
569
- idleTtlMs: gateConfig.auth.session.idleTtlMs,
570
- absoluteTtlMs: gateConfig.auth.session.absoluteTtlMs,
571
- singleSessionPerUser: gateConfig.auth.session.singleSessionPerUser,
572
- },
573
- password: gateConfig.auth.password,
574
- passwordPolicy: gateConfig.auth.passwordPolicy,
575
- breachCheck: gateConfig.auth.breachCheck,
576
- }),
577
- appPluginScope,
550
+ const { wireGateAuth } = requirePackageModule<typeof import("./app-auth.ts")>(
551
+ "kernel/app-auth",
552
+ "app-auth",
578
553
  );
554
+ wiredAuth = wireGateAuth({
555
+ gateConfig: { ...gateConfig, auth: gateConfig.auth },
556
+ now: options.fx?.now,
557
+ });
558
+ authMaterialization = wiredAuth.materialization;
559
+ authBinding = wiredAuth.authBinding;
560
+ if (authMaterialization) {
561
+ adopted.push(...authMaterialization.bindings);
562
+ }
563
+ if (wiredAuth.authPlugin) {
564
+ applyPlugin(pluginRegistry, wiredAuth.authPlugin, appPluginScope);
565
+ }
579
566
  }
580
567
 
581
568
  const smart = createRouter<Binding>(options.router ?? "default");
@@ -671,29 +658,10 @@ export function oke(options: OkeOptions): OkeApp {
671
658
  let bootPromise: Promise<BootResult> | undefined;
672
659
  let readyState: ReadyState = "booting";
673
660
  let bootEnv: BootOptions["env"] = options.env;
674
- let authBinding: AppAuthBinding | undefined =
675
- gateConfig.auth !== undefined
676
- ? createAppAuthBinding({
677
- secret: gateConfig.auth.secret,
678
- sessions: authMaterialization?.ctx.sessions ?? gateConfig.auth.sessions,
679
- now: gateConfig.auth.now ?? options.fx?.now,
680
- })
681
- : undefined;
682
- if (authBinding) {
683
- setActiveGateAuthContext({
684
- secret: authBinding.secret,
685
- sessions: authBinding.sessions,
686
- now: authBinding.now,
687
- passwordPolicy: gateConfig.auth?.passwordPolicy,
688
- password: gateConfig.auth?.password,
689
- breachCheck: gateConfig.auth?.breachCheck,
690
- });
691
- } else {
692
- setActiveGateAuthContext(undefined);
693
- }
694
661
  // Fallback journal for pre-boot / `autoBoot: false` unit tests. A booted
695
662
  // app replaces this with the bound `drivers.journal` store (postgres etc.).
696
- const fallbackJournalStore = createMemoryJournalStore();
663
+ // Constructed on first use so non-durable HTTP apps skip the alloc.
664
+ let fallbackJournalStore: JournalStore | undefined;
697
665
  const fallbackJournalInstanceId = `app-${crypto.randomUUID()}`;
698
666
  const sleepingRuns = new Map<
699
667
  string,
@@ -715,6 +683,7 @@ export function oke(options: OkeOptions): OkeApp {
715
683
  if (bound) {
716
684
  return { store: bound.store, instanceId: bound.instanceId, leaseMs: bound.leaseMs };
717
685
  }
686
+ fallbackJournalStore ??= createMemoryJournalStore();
718
687
  return {
719
688
  store: fallbackJournalStore,
720
689
  instanceId: fallbackJournalInstanceId,
@@ -978,12 +947,9 @@ export function oke(options: OkeOptions): OkeApp {
978
947
  readyState = "ready";
979
948
  }
980
949
  // Prefer the booted clock for access-token expiry checks.
981
- if (authBinding) {
982
- authBinding = createAppAuthBinding({
983
- secret: authBinding.secret,
984
- sessions: authBinding.sessions,
985
- now: result.clock?.now.bind(result.clock) ?? authBinding.now,
986
- });
950
+ if (wiredAuth && authBinding) {
951
+ const now = result.clock?.now.bind(result.clock) ?? authBinding.now;
952
+ authBinding = wiredAuth.rebind(now);
987
953
  }
988
954
  return result;
989
955
  }
@@ -1144,21 +1110,24 @@ export function oke(options: OkeOptions): OkeApp {
1144
1110
 
1145
1111
  const binding = authBinding;
1146
1112
  const cookieOpts = gateConfig.auth?.cookies;
1113
+ const verifyBearer = wiredAuth?.verifyBearerToken;
1114
+ const cookieToken = wiredAuth?.tokenFromCookieHeader;
1147
1115
  const elementHooks = createElementPipelineHooks({
1148
1116
  gates: booted.gate,
1149
1117
  principals,
1150
1118
  telemetry,
1151
1119
  allowTestPrincipals: testMode,
1152
- verifyBearer: binding ? async (token) => verifyBearerToken(binding, token) : undefined,
1120
+ verifyBearer:
1121
+ binding && verifyBearer ? async (token) => verifyBearer(binding, token) : undefined,
1153
1122
  // Phase 1a: opt-in cookie → Bearer when Authorization is absent.
1154
1123
  resolveToken:
1155
- binding && cookieOpts?.enabled
1124
+ binding && cookieOpts?.enabled && cookieToken
1156
1125
  ? (request) => {
1157
1126
  const header = request.headers.get("authorization");
1158
1127
  if (header?.startsWith("Bearer ")) {
1159
1128
  return header.slice("Bearer ".length).trim() || undefined;
1160
1129
  }
1161
- return tokenFromCookieHeader(request.headers.get("cookie"), cookieOpts);
1130
+ return cookieToken(request.headers.get("cookie"), cookieOpts);
1162
1131
  }
1163
1132
  : undefined,
1164
1133
  });
@@ -79,7 +79,7 @@ describe("boot — lazy element needs", () => {
79
79
  expect(needs.signal).toBe(false);
80
80
  });
81
81
 
82
- test("oke() Store-only graph stays under the prior 54.3 kB baseline", async () => {
82
+ test("oke() Store-only graph stays under the prior 39 kB baseline", async () => {
83
83
  const dir = await mkdtemp(join(tmpdir(), "oke-store-only-"));
84
84
  const entry = join(dir, "entry.ts");
85
85
  const appPath = join(import.meta.dir, "app.ts");
@@ -116,23 +116,9 @@ describe("boot — lazy element needs", () => {
116
116
  if (raw.byteLength === 0) continue;
117
117
  total += Bun.gzipSync(new Uint8Array(raw)).byteLength;
118
118
  }
119
- // Rebased after fx.sendOtp/verifyOtp on the shared fx surface (~50.1 kB
120
- // gzip), durable-journal lease surface (~51.2 kB), fx.deliverOtp +
121
- // unified otp() (~50.9 kB → 53 kB), then fx.runs / SLO window on the
122
- // shared surface (~53.3 kB → 54 kB cap). Clock/channel/journal drivers
123
- // stay lazy-bound; far below eager bind. Rebased again for the
124
- // store/vault/signal/channel auto-registry drain (`element-registries.ts`
125
- // + the `oke()` merge/dedup logic) — every app now carries this
126
- // regardless of which elements it uses, since the drain must run
127
- // synchronously at construction (same isolation guarantee as the
128
- // `on()` binding registry) and can't be deferred behind a lazy import
129
- // (~54.0 kB → 54.3 kB cap). Rebased again for `assertAdoptBarrelFresh`
130
- // + `tryListFlowsUnits` in `boot.ts` (`.adopt()` barrel staleness
131
- // check, OKE1009) — part of the already-lazily-loaded boot graph, not
132
- // the `oke()` construction path, but `Bun.build` still traces and
133
- // sums the dynamic-import chunk (~54.3 kB → 54.7 kB → 55.0 kB after
134
- // ConfigEnv local/docker → dev/test/prod + sqlite removal).
135
- expect(total).toBeLessThan(55_000);
119
+ // Rebased after lazy `gate.auth` / auth-config chunks + `gate.public`
120
+ // decoupling from kv-lua strategies (~38.9 kB gzip with export externals).
121
+ expect(total).toBeLessThan(40_000);
136
122
  } finally {
137
123
  await rm(dir, { recursive: true, force: true });
138
124
  }
@@ -22,6 +22,9 @@ const ENTRIES: readonly { readonly src: string; readonly out: string }[] = [
22
22
  { src: "src/i18n-entry.ts", out: "dist/i18n-entry.js" },
23
23
  { src: "src/compiler-entry.ts", out: "dist/compiler-entry.js" },
24
24
  { src: "src/journal-entry.ts", out: "dist/journal-entry.js" },
25
+ // Lazy sync chunks — loaded via requirePackageModule when gate.auth is set.
26
+ { src: "src/kernel/app-auth.ts", out: "dist/app-auth.js" },
27
+ { src: "src/auth/config.ts", out: "dist/auth-config.js" },
25
28
  ];
26
29
 
27
30
  /**
@@ -0,0 +1,79 @@
1
+ /**
2
+ * Sync-load package-local modules that Bun.build leaves as runtime requires
3
+ * (not inlined). Bun `src/` first; published `dist/` chunks for `"import"`.
4
+ *
5
+ * Uses Bun APIs (`import.meta.dir`, `import.meta.require`) and try/catch load
6
+ * instead of exists()+require — Bun’s preferred pattern (the exists syscall
7
+ * is an extra round trip).
8
+ */
9
+
10
+ import { dirname, join } from "node:path";
11
+
12
+ let cachedRoot: string | undefined;
13
+
14
+ /**
15
+ * Try a sync `import.meta.require`; return `undefined` only when the file is missing.
16
+ *
17
+ * @typeParam T - Module namespace shape
18
+ * @param path - Absolute module path
19
+ */
20
+ function tryRequire<T>(path: string): T | undefined {
21
+ try {
22
+ return import.meta.require(path) as T;
23
+ } catch (err) {
24
+ const code =
25
+ err !== null && typeof err === "object" && "code" in err
26
+ ? String((err as { code?: unknown }).code)
27
+ : undefined;
28
+ if (
29
+ code === "MODULE_NOT_FOUND" ||
30
+ code === "ENOENT" ||
31
+ (err instanceof Error && /Cannot find module|ENOENT/i.test(err.message))
32
+ ) {
33
+ return undefined;
34
+ }
35
+ throw err;
36
+ }
37
+ }
38
+
39
+ /**
40
+ * Walk up from this module to the okengine package root (`package.json` name).
41
+ */
42
+ export function okengineRoot(): string {
43
+ if (cachedRoot) return cachedRoot;
44
+ let dir = import.meta.dir;
45
+ for (let i = 0; i < 12; i++) {
46
+ const pkg = tryRequire<{ name?: string }>(join(dir, "package.json"));
47
+ if (pkg?.name === "okengine") {
48
+ cachedRoot = dir;
49
+ return dir;
50
+ }
51
+ const parent = dirname(dir);
52
+ if (parent === dir) break;
53
+ dir = parent;
54
+ }
55
+ throw new Error("okengine: package root not found from " + import.meta.dir);
56
+ }
57
+
58
+ /**
59
+ * Synchronously load `src/<rel>.ts` (Bun) or published `dist/<distName>.js`.
60
+ *
61
+ * @typeParam T - Module namespace shape
62
+ * @param relSrc - Path under `src/` without extension (e.g. `auth/config`)
63
+ * @param distName - Filename under `dist/` without extension (e.g. `auth-config`)
64
+ */
65
+ export function requirePackageModule<T>(relSrc: string, distName: string): T {
66
+ const root = okengineRoot();
67
+ const srcPath = join(root, "src", `${relSrc}.ts`);
68
+ const distPath = join(root, "dist", `${distName}.js`);
69
+ // Bun native TS — prefer source so local `oke` / tests stay on one graph.
70
+ if (typeof Bun !== "undefined") {
71
+ const fromSrc = tryRequire<T>(srcPath);
72
+ if (fromSrc !== undefined) return fromSrc;
73
+ }
74
+ const fromDist = tryRequire<T>(distPath);
75
+ if (fromDist !== undefined) return fromDist;
76
+ const fallback = tryRequire<T>(srcPath);
77
+ if (fallback !== undefined) return fallback;
78
+ throw new Error(`okengine: missing lazy module src/${relSrc}.ts or dist/${distName}.js`);
79
+ }