okengine 0.14.1 → 0.16.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/package.json +3 -4
- package/site/content/docs/elements/ai.mdx +2 -1
- package/site/content/docs/elements/channel.mdx +1 -0
- package/site/content/docs/elements/clock.mdx +3 -0
- package/site/content/docs/elements/flow.mdx +9 -0
- package/site/content/docs/elements/gate.mdx +80 -22
- package/site/content/docs/elements/signal.mdx +4 -4
- package/site/content/docs/elements/store.mdx +84 -27
- package/site/content/docs/elements/vault.mdx +4 -0
- package/site/content/docs/get-started/basic-usage.mdx +22 -13
- package/site/content/docs/get-started/index.mdx +5 -0
- package/site/content/docs/get-started/introduction.mdx +11 -3
- package/site/content/docs/get-started/meta.json +1 -1
- package/site/content/docs/get-started/project-structure.mdx +932 -0
- package/site/content/docs/reference/cli.md +13 -10
- package/site/content/docs/reference/client.mdx +28 -21
- package/site/content/docs/reference/fx.mdx +29 -18
- package/site/content/docs/reference/security.md +2 -2
- package/src/auth/api-key-sql.test.ts +47 -0
- package/src/auth/api-key-sql.ts +254 -0
- package/src/auth/api-keys.ts +273 -14
- package/src/auth/auth.test.ts +56 -0
- package/src/auth/bindings.ts +14 -5
- package/src/auth/config.ts +5 -5
- package/src/auth/cross-plane.ts +10 -3
- package/src/auth/index.ts +25 -1
- package/src/auth/schema.ts +8 -0
- package/src/cli/ai-setup/ai-setup.test.ts +62 -0
- package/src/cli/ai-setup/apply.ts +32 -1
- package/src/cli/attach-host-console.test.ts +40 -2
- package/src/cli/attach-host-console.ts +9 -0
- package/src/cli/build.test.ts +4 -2
- package/src/cli/db-seed.ts +48 -15
- package/src/cli/db.test.ts +104 -0
- package/src/cli/db.ts +122 -10
- package/src/cli/dev-controls.test.ts +20 -0
- package/src/cli/dev-controls.ts +13 -0
- package/src/cli/dev.test.ts +15 -4
- package/src/cli/dev.ts +102 -29
- package/src/cli/ensure-drizzle-config.ts +2 -1
- package/src/cli/tui/DevLive.tsx +12 -2
- package/src/cli/tui/keys.test.ts +2 -1
- package/src/cli/tui/keys.ts +3 -2
- package/src/compiler/aot.test.ts +1 -1
- package/src/compiler/effects-infer.ts +17 -2
- package/src/compiler/extract.test.ts +409 -33
- package/src/compiler/extract.ts +318 -67
- package/src/compiler/flow-path.test.ts +96 -0
- package/src/compiler/flow-path.ts +173 -0
- package/src/compiler/generate-adopt.test.ts +112 -3
- package/src/compiler/generate-adopt.ts +280 -26
- package/src/compiler/schema-from-ast.ts +609 -0
- package/src/console/server/access.test.ts +116 -8
- package/src/console/server/access.ts +70 -23
- package/src/console/server/app.ts +1 -0
- package/src/console/server/bind.ts +2 -2
- package/src/console/server/console.test.ts +3 -0
- 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 +233 -98
- package/src/console/server/gates.test.ts +24 -0
- package/src/console/server/gates.ts +8 -21
- package/src/console/server/index.ts +1 -0
- package/src/console/server/invoke-user-flow.test.ts +3 -1
- package/src/console/server/invoke-user-flow.ts +17 -2
- package/src/console/server/iso-at.test.ts +9 -0
- package/src/console/server/iso-at.ts +11 -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/security-headers.ts +6 -3
- package/src/console/server/spa-proxy.test.ts +1 -0
- package/src/console/server/sql-catalog.ts +147 -7
- package/src/console/server/sql-rls.test.ts +16 -0
- package/src/console/server/state.ts +37 -55
- package/src/console/server/store.test.ts +7 -4
- package/src/console/server/store.ts +142 -58
- package/src/console/ui-next/dist/assets/access-page-CAGHrA9H.js +4 -0
- package/src/console/ui-next/dist/assets/agent-disclosure-BP0Y0Sux.js +1 -0
- 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/{duration-tone-oiRxPVsZ.js → duration-tone-sC3lGABz.js} +1 -1
- 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-B-OUtiAu.js +1 -0
- package/src/console/ui-next/dist/assets/{highlighted-json-CvDPvveV.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-CGoZkILK.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-BDyXalNR.js +4 -0
- package/src/console/ui-next/dist/assets/react-D8E3mtu1.js +1 -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-CL_X0GCj.js → skeleton-D-czQJT6.js} +1 -1
- package/src/console/ui-next/dist/assets/store-page-Xh8Kn3rx.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-DkOXA12R.js +54 -0
- package/src/console/ui-next/dist/assets/units-page-Dpk40kOQ.js +1 -0
- package/src/console/ui-next/dist/assets/{use-vault-list-Cl79j_ku.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-B1dB9Ft0.js +2 -0
- package/src/console/ui-next/dist/index.html +12 -10
- package/src/console/ui-next/index.html +3 -3
- package/src/console/ui-next/src/client.ts +202 -7
- package/src/console/ui-next/src/components/motion/table/index.tsx +16 -0
- package/src/console/ui-next/src/components/motion/table/types.ts +4 -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/kbd.tsx +2 -2
- 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/traces/http-method.ts +19 -0
- package/src/console/ui-next/src/features/flows/traces/trace-detail.test.ts +19 -1
- package/src/console/ui-next/src/features/flows/traces/traces-pane.tsx +10 -3
- package/src/console/ui-next/src/features/not-found/not-found-page.tsx +1 -1
- package/src/console/ui-next/src/features/store/detail/browse-section.tsx +45 -1
- package/src/console/ui-next/src/features/store/detail/resource-panel.tsx +78 -16
- package/src/console/ui-next/src/features/store/detail/store-row-detail-sheet.tsx +155 -14
- package/src/console/ui-next/src/features/store/explorer/store-tree.tsx +33 -133
- package/src/console/ui-next/src/features/store/files/file-preview.tsx +4 -1
- package/src/console/ui-next/src/features/store/grid/rls-policy-sheet.tsx +326 -165
- package/src/console/ui-next/src/features/store/grid/sql-insert-sheet.tsx +6 -0
- package/src/console/ui-next/src/features/store/grid/store-data-grid.tsx +50 -3
- package/src/console/ui-next/src/features/store/lib/files-meta.test.ts +3 -0
- package/src/console/ui-next/src/features/store/lib/files-meta.ts +12 -0
- package/src/console/ui-next/src/features/store/lib/query-defaults.test.ts +25 -0
- package/src/console/ui-next/src/features/store/lib/query-defaults.ts +85 -3
- package/src/console/ui-next/src/features/store/lib/query-gate.ts +30 -4
- package/src/console/ui-next/src/features/store/lib/rls-policy.test.ts +139 -0
- package/src/console/ui-next/src/features/store/lib/rls-policy.ts +223 -21
- package/src/console/ui-next/src/features/store/lib/schema-graph.ts +4 -0
- package/src/console/ui-next/src/features/store/lib/sql-catalog.test.ts +24 -0
- package/src/console/ui-next/src/features/store/lib/sql-catalog.ts +19 -0
- package/src/console/ui-next/src/features/store/lib/store-tree.test.ts +6 -0
- package/src/console/ui-next/src/features/store/lib/store-tree.ts +13 -0
- package/src/console/ui-next/src/features/store/query/query-console.tsx +48 -14
- package/src/console/ui-next/src/features/store/query/query-gate-menu.tsx +19 -20
- package/src/console/ui-next/src/features/store/query/query-gate-parts.tsx +2 -1
- package/src/console/ui-next/src/features/store/query/query-results.tsx +110 -83
- package/src/console/ui-next/src/features/store/schema/schema-table-node.tsx +17 -0
- package/src/console/ui-next/src/features/units/call/call-api-panel.tsx +112 -72
- package/src/console/ui-next/src/features/units/lib/contract-input.test.ts +206 -1
- package/src/console/ui-next/src/features/units/lib/contract-input.ts +55 -0
- package/src/console/ui-next/src/features/vault/lib/types.ts +1 -1
- 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-store.ts +43 -15
- package/src/console/xss.gate.test.ts +1 -0
- package/src/docker/compose-health.test.ts +27 -0
- package/src/docker/compose-health.ts +19 -3
- package/src/docker/docker.test.ts +3 -0
- package/src/docker/recipes/pgdog.ts +2 -0
- package/src/drivers/clock-postgres.ts +13 -9
- package/src/drivers/instances-postgres.ts +4 -6
- package/src/drivers/journal-postgres.ts +13 -9
- package/src/drivers/pg-rls.test.ts +87 -0
- package/src/drivers/pg-rls.ts +233 -7
- package/src/drivers/pglite.ts +26 -3
- package/src/drivers/postgres.test.ts +23 -0
- package/src/drivers/postgres.ts +133 -9
- package/src/drivers/types.ts +10 -0
- package/src/elements/clock/declare.ts +26 -1
- package/src/elements/gate/boot.ts +5 -5
- package/src/elements/gate/declare.ts +38 -4
- package/src/elements/gate/flatten.ts +1 -1
- package/src/elements/gate/runtime.ts +1 -1
- package/src/elements/store/cache.ts +1 -1
- package/src/elements/store/declare.ts +9 -6
- package/src/elements/store/emit-drizzle.ts +44 -8
- package/src/elements/store/resource.test.ts +1 -1
- package/src/elements/store/rls-identity.test.ts +95 -0
- package/src/elements/store/rls-identity.ts +116 -0
- package/src/elements/store/runtime.ts +18 -0
- package/src/elements/store/schema-decl.test.ts +37 -0
- package/src/elements/store/schema-decl.ts +260 -41
- package/src/elements/store/seed.ts +1 -1
- package/src/elements/store/sql-condition.test.ts +12 -0
- package/src/elements/store/sql-condition.ts +26 -2
- package/src/elements/store/sql-rls-isolation.test.ts +165 -0
- package/src/elements/store/sql-rls-stamp.test.ts +117 -0
- package/src/elements/store/sql-select.types.test.ts +71 -0
- package/src/elements/store/sql-session.ts +135 -35
- package/src/elements/store/upsert-app.test.ts +1 -1
- package/src/elements/store.ts +17 -0
- package/src/http.ts +20 -3
- package/src/index.ts +11 -1
- package/src/kernel/adopt-barrel-fresh.test.ts +1 -1
- package/src/kernel/adopt-routes.ts +30 -2
- package/src/kernel/api-key-host-persist.test.ts +143 -0
- package/src/kernel/app.ts +145 -16
- package/src/kernel/auth-resolve.ts +10 -5
- package/src/kernel/auto-cache.test.ts +8 -8
- package/src/kernel/auto-registry.test.ts +49 -9
- package/src/kernel/boot.test.ts +5 -5
- package/src/kernel/boot.ts +2 -2
- package/src/kernel/effects-stamping.test.ts +3 -3
- package/src/kernel/element-registries.ts +12 -5
- package/src/kernel/errors.ts +24 -0
- package/src/kernel/flow-units.ts +64 -0
- package/src/kernel/flow.ts +29 -17
- package/src/kernel/fx-auth-keys.test.ts +88 -0
- package/src/kernel/fx-auth-keys.ts +198 -0
- package/src/kernel/fx.test.ts +17 -1
- package/src/kernel/fx.ts +59 -18
- package/src/kernel/horizontal-child.ts +4 -4
- package/src/kernel/http-path-pending.ts +20 -0
- package/src/kernel/http-resource.ts +4 -0
- package/src/kernel/http-stream.test.ts +4 -4
- package/src/kernel/pipeline-api-key.test.ts +192 -7
- package/src/kernel/pipeline.test.ts +2 -2
- package/src/kernel/pipeline.ts +4 -2
- package/src/kernel/plugin-elements.test.ts +1 -1
- package/src/kernel/plugin-needs.test.ts +1 -1
- package/src/kernel/ready.test.ts +2 -2
- package/src/kernel/resource-mount.test.ts +14 -0
- package/src/kernel/run-duration.test.ts +2 -2
- package/src/kernel/stamp-http.test.ts +79 -0
- package/src/kernel/stamp-http.ts +75 -0
- package/src/kernel/triggers.ts +52 -27
- package/src/manifest/types.ts +22 -3
- package/src/release/build-lib.ts +1 -0
- package/src/release/measure.ts +2 -2
- package/src/runtime/json-code-block.test.ts +2 -2
- package/src/runtime/serve.test.ts +4 -4
- package/src/term.test.ts +52 -4
- package/src/term.ts +125 -17
- package/src/test/reset-element-registries.ts +8 -3
- package/src/cli/ask-seed.test.ts +0 -96
- package/src/cli/ask-seed.ts +0 -82
- package/src/console/ui-next/dist/assets/cache-glyph-BanhLsEY.js +0 -1
- package/src/console/ui-next/dist/assets/call-pii-button-bqkxMrJH.js +0 -1
- package/src/console/ui-next/dist/assets/collapsible-DYb0xU8C.js +0 -1
- package/src/console/ui-next/dist/assets/confirm-sheet-DDCRmG62.js +0 -1
- package/src/console/ui-next/dist/assets/explorer-empty-HjTnVQoR.js +0 -1
- package/src/console/ui-next/dist/assets/flows-page-DxDsOd4f.js +0 -1
- package/src/console/ui-next/dist/assets/http-method-CJCBYL2j.js +0 -1
- package/src/console/ui-next/dist/assets/index-Ce6WKWKM.js +0 -66
- package/src/console/ui-next/dist/assets/index-D4Ldtj79.css +0 -2
- package/src/console/ui-next/dist/assets/link-DF7SZ9Ek.js +0 -1
- package/src/console/ui-next/dist/assets/observability-page-BEZDzyYh.js +0 -4
- package/src/console/ui-next/dist/assets/query-gate-parts-1m8m1iNp.js +0 -2
- package/src/console/ui-next/dist/assets/react-B1ML8gxg.js +0 -1
- package/src/console/ui-next/dist/assets/replica-lag-C8_BRt2x.js +0 -18
- package/src/console/ui-next/dist/assets/sheet-form-D-ata7jy.js +0 -1
- package/src/console/ui-next/dist/assets/shortcut-Cgzuv4k1.js +0 -1
- package/src/console/ui-next/dist/assets/shortcut-keys-DO4IsVqv.js +0 -1
- package/src/console/ui-next/dist/assets/store-page-v3LXdYpr.js +0 -45
- package/src/console/ui-next/dist/assets/trace-detail-sheet-DFLFfUUX.js +0 -2
- package/src/console/ui-next/dist/assets/units-page-C0gW6Kdo.js +0 -1
- package/src/console/ui-next/dist/assets/vault-page-DuKzqwzW.js +0 -2
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Boot-level: API-key Bearer stamps WideEvent.principal with the
|
|
2
|
+
* Boot-level: API-key Bearer stamps WideEvent.principal with the issuer
|
|
3
|
+
* (`creatorId`) and `dimensions.api_key` with the key id.
|
|
3
4
|
* Session JWT stamps the session subject — never a key id.
|
|
4
5
|
*/
|
|
5
6
|
|
|
@@ -22,7 +23,7 @@ async function lastEvent(runs: ReturnType<typeof createRunsRuntime>) {
|
|
|
22
23
|
}
|
|
23
24
|
|
|
24
25
|
describe("pipeline — API key Bearer identity", () => {
|
|
25
|
-
test("API key Bearer stamps WideEvent.principal with the
|
|
26
|
+
test("API key Bearer stamps WideEvent.principal with the issuer", async () => {
|
|
26
27
|
resetBindings();
|
|
27
28
|
resetFlowSeq();
|
|
28
29
|
|
|
@@ -70,9 +71,193 @@ describe("pipeline — API key Bearer identity", () => {
|
|
|
70
71
|
expect(res.status).toBe(200);
|
|
71
72
|
|
|
72
73
|
const event = await lastEvent(runs);
|
|
73
|
-
expect(event?.principal).toBe("
|
|
74
|
+
expect(event?.principal).toBe("u1");
|
|
74
75
|
expect(event?.dimensions.api_key).toBe("key_demo");
|
|
75
|
-
expect(event?.dimensions.principal).toBe("
|
|
76
|
+
expect(event?.dimensions.principal).toBe("u1");
|
|
77
|
+
|
|
78
|
+
await app.stop();
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test("key-authenticated request stamps fx.auth.userId as issuer and apiKeyId as the key", async () => {
|
|
82
|
+
resetBindings();
|
|
83
|
+
resetFlowSeq();
|
|
84
|
+
|
|
85
|
+
on(
|
|
86
|
+
http.get("/who").gate(member),
|
|
87
|
+
flow("secure.who", {
|
|
88
|
+
do: (_input, fx) => ({
|
|
89
|
+
userId: fx.auth.userId,
|
|
90
|
+
apiKeyId: fx.auth.apiKeyId ?? null,
|
|
91
|
+
}),
|
|
92
|
+
}),
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
const apiKeys = createApiKeyStore();
|
|
96
|
+
const created = await createApiKey(apiKeys, {
|
|
97
|
+
plane: "user",
|
|
98
|
+
name: "who",
|
|
99
|
+
scopes: ["member"],
|
|
100
|
+
creatorId: "u1",
|
|
101
|
+
creatorScopes: ["member"],
|
|
102
|
+
id: "key_who",
|
|
103
|
+
});
|
|
104
|
+
const app = oke({
|
|
105
|
+
name: "api-key-who",
|
|
106
|
+
gate: {
|
|
107
|
+
auth: {
|
|
108
|
+
secret: "hmac-secret-for-tests",
|
|
109
|
+
sessions: createSessionStore(),
|
|
110
|
+
apiKeyStore: apiKeys,
|
|
111
|
+
http: false,
|
|
112
|
+
},
|
|
113
|
+
policies: [member],
|
|
114
|
+
},
|
|
115
|
+
env: "test",
|
|
116
|
+
startScheduler: false,
|
|
117
|
+
});
|
|
118
|
+
await app.boot({ env: "test", gates: [member], startScheduler: false });
|
|
119
|
+
|
|
120
|
+
const res = await app.fetch(
|
|
121
|
+
new Request("http://localhost/who", {
|
|
122
|
+
method: "GET",
|
|
123
|
+
headers: { authorization: `Bearer ${created.secret}` },
|
|
124
|
+
}),
|
|
125
|
+
);
|
|
126
|
+
expect(res.status).toBe(200);
|
|
127
|
+
const body = (await res.json()) as { data: { userId: string; apiKeyId: string } };
|
|
128
|
+
expect(body.data.userId).toBe("u1");
|
|
129
|
+
expect(body.data.apiKeyId).toBe("key_who");
|
|
130
|
+
|
|
131
|
+
await app.stop();
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
test("scope A is Forbidden on gate.scope(B) and 200 on gate.scope(A)", async () => {
|
|
135
|
+
resetBindings();
|
|
136
|
+
resetFlowSeq();
|
|
137
|
+
|
|
138
|
+
const scopeA = gate.scope("scope:a");
|
|
139
|
+
const scopeB = gate.scope("scope:b");
|
|
140
|
+
on(http.get("/a").gate(scopeA), flow("secure.a", { do: () => ({ ok: true }) }));
|
|
141
|
+
on(http.get("/b").gate(scopeB), flow("secure.b", { do: () => ({ ok: true }) }));
|
|
142
|
+
|
|
143
|
+
const apiKeys = createApiKeyStore();
|
|
144
|
+
const created = await createApiKey(apiKeys, {
|
|
145
|
+
plane: "user",
|
|
146
|
+
name: "scoped",
|
|
147
|
+
scopes: ["scope:a"],
|
|
148
|
+
creatorId: "u1",
|
|
149
|
+
creatorScopes: ["scope:a", "scope:b"],
|
|
150
|
+
});
|
|
151
|
+
const app = oke({
|
|
152
|
+
name: "api-key-scopes",
|
|
153
|
+
gate: {
|
|
154
|
+
auth: {
|
|
155
|
+
secret: "hmac-secret-for-tests",
|
|
156
|
+
sessions: createSessionStore(),
|
|
157
|
+
apiKeyStore: apiKeys,
|
|
158
|
+
http: false,
|
|
159
|
+
},
|
|
160
|
+
policies: [scopeA, scopeB],
|
|
161
|
+
},
|
|
162
|
+
env: "test",
|
|
163
|
+
startScheduler: false,
|
|
164
|
+
});
|
|
165
|
+
await app.boot({ env: "test", gates: [scopeA, scopeB], startScheduler: false });
|
|
166
|
+
|
|
167
|
+
const a = await app.fetch(
|
|
168
|
+
new Request("http://localhost/a", {
|
|
169
|
+
method: "GET",
|
|
170
|
+
headers: { authorization: `Bearer ${created.secret}` },
|
|
171
|
+
}),
|
|
172
|
+
);
|
|
173
|
+
const b = await app.fetch(
|
|
174
|
+
new Request("http://localhost/b", {
|
|
175
|
+
method: "GET",
|
|
176
|
+
headers: { authorization: `Bearer ${created.secret}` },
|
|
177
|
+
}),
|
|
178
|
+
);
|
|
179
|
+
expect(a.status).toBe(200);
|
|
180
|
+
expect(b.status).toBe(403);
|
|
181
|
+
|
|
182
|
+
await app.stop();
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
test("allowlist miss and over-rate → 401, no api_key dimension", async () => {
|
|
186
|
+
resetBindings();
|
|
187
|
+
resetFlowSeq();
|
|
188
|
+
|
|
189
|
+
on(http.get("/secure").gate(member), flow("secure.limit", { do: () => ({ ok: true }) }));
|
|
190
|
+
|
|
191
|
+
const apiKeys = createApiKeyStore();
|
|
192
|
+
const allowlisted = await createApiKey(apiKeys, {
|
|
193
|
+
plane: "user",
|
|
194
|
+
name: "allow",
|
|
195
|
+
scopes: ["member"],
|
|
196
|
+
creatorId: "u1",
|
|
197
|
+
creatorScopes: ["member"],
|
|
198
|
+
id: "key_allow",
|
|
199
|
+
ipAllowlist: ["203.0.113.10"],
|
|
200
|
+
});
|
|
201
|
+
const rated = await createApiKey(apiKeys, {
|
|
202
|
+
plane: "user",
|
|
203
|
+
name: "rated",
|
|
204
|
+
scopes: ["member"],
|
|
205
|
+
creatorId: "u1",
|
|
206
|
+
creatorScopes: ["member"],
|
|
207
|
+
id: "key_rate",
|
|
208
|
+
rateLimit: { max: 1, per: "1m" },
|
|
209
|
+
});
|
|
210
|
+
const runs = createRunsRuntime({ driver: memoryRunsDriver });
|
|
211
|
+
await runs.open();
|
|
212
|
+
const app = oke({
|
|
213
|
+
name: "api-key-limits",
|
|
214
|
+
gate: {
|
|
215
|
+
auth: {
|
|
216
|
+
secret: "hmac-secret-for-tests",
|
|
217
|
+
sessions: createSessionStore(),
|
|
218
|
+
apiKeyStore: apiKeys,
|
|
219
|
+
http: false,
|
|
220
|
+
},
|
|
221
|
+
policies: [member],
|
|
222
|
+
},
|
|
223
|
+
runs,
|
|
224
|
+
env: "test",
|
|
225
|
+
startScheduler: false,
|
|
226
|
+
});
|
|
227
|
+
await app.boot({ env: "test", gates: [member], runs, startScheduler: false });
|
|
228
|
+
|
|
229
|
+
const miss = await app.fetch(
|
|
230
|
+
new Request("http://localhost/secure", {
|
|
231
|
+
method: "GET",
|
|
232
|
+
headers: {
|
|
233
|
+
authorization: `Bearer ${allowlisted.secret}`,
|
|
234
|
+
"x-forwarded-for": "198.51.100.7",
|
|
235
|
+
},
|
|
236
|
+
}),
|
|
237
|
+
);
|
|
238
|
+
expect(miss.status).toBe(401);
|
|
239
|
+
|
|
240
|
+
const first = await app.fetch(
|
|
241
|
+
new Request("http://localhost/secure", {
|
|
242
|
+
method: "GET",
|
|
243
|
+
headers: { authorization: `Bearer ${rated.secret}` },
|
|
244
|
+
}),
|
|
245
|
+
);
|
|
246
|
+
const second = await app.fetch(
|
|
247
|
+
new Request("http://localhost/secure", {
|
|
248
|
+
method: "GET",
|
|
249
|
+
headers: { authorization: `Bearer ${rated.secret}` },
|
|
250
|
+
}),
|
|
251
|
+
);
|
|
252
|
+
expect(first.status).toBe(200);
|
|
253
|
+
expect(second.status).toBe(401);
|
|
254
|
+
|
|
255
|
+
const events = await runs.all();
|
|
256
|
+
for (const event of events) {
|
|
257
|
+
if (event.dimensions.error_code) {
|
|
258
|
+
expect(event.dimensions.api_key).toBeUndefined();
|
|
259
|
+
}
|
|
260
|
+
}
|
|
76
261
|
|
|
77
262
|
await app.stop();
|
|
78
263
|
});
|
|
@@ -206,7 +391,7 @@ describe("pipeline — API key Bearer identity", () => {
|
|
|
206
391
|
await app.stop();
|
|
207
392
|
});
|
|
208
393
|
|
|
209
|
-
test("operator-plane key stamps fx.operator.id as the
|
|
394
|
+
test("operator-plane key stamps fx.operator.id as the issuer", async () => {
|
|
210
395
|
resetBindings();
|
|
211
396
|
resetFlowSeq();
|
|
212
397
|
|
|
@@ -254,10 +439,10 @@ describe("pipeline — API key Bearer identity", () => {
|
|
|
254
439
|
);
|
|
255
440
|
expect(res.status).toBe(200);
|
|
256
441
|
const body = (await res.json()) as { data: { operatorId: string } };
|
|
257
|
-
expect(body.data.operatorId).toBe("
|
|
442
|
+
expect(body.data.operatorId).toBe("op1");
|
|
258
443
|
|
|
259
444
|
const event = await lastEvent(runs);
|
|
260
|
-
expect(event?.principal).toBe("
|
|
445
|
+
expect(event?.principal).toBe("op1");
|
|
261
446
|
expect(event?.plane).toBe("operator");
|
|
262
447
|
expect(event?.dimensions.api_key).toBe("key_ops");
|
|
263
448
|
|
|
@@ -65,9 +65,9 @@ describe("gateDenialFailure", () => {
|
|
|
65
65
|
});
|
|
66
66
|
});
|
|
67
67
|
|
|
68
|
-
describe("http trigger — .
|
|
68
|
+
describe("http trigger — .public()", () => {
|
|
69
69
|
test("attaches the public sentinel without calling gate()", () => {
|
|
70
|
-
const trigger = http.get("/health").
|
|
70
|
+
const trigger = http.get("/health").public();
|
|
71
71
|
expect(trigger.gates.map((g) => (typeof g === "string" ? g : g.name))).toEqual(["public"]);
|
|
72
72
|
expect(trigger.live().isLive).toBe(true);
|
|
73
73
|
});
|
package/src/kernel/pipeline.ts
CHANGED
|
@@ -24,6 +24,7 @@ export interface PrincipalBag {
|
|
|
24
24
|
userId: string | null;
|
|
25
25
|
scopes: Set<string>;
|
|
26
26
|
verified?: boolean;
|
|
27
|
+
apiKeyId?: string | null;
|
|
27
28
|
};
|
|
28
29
|
readonly operator: {
|
|
29
30
|
id: string | null;
|
|
@@ -53,7 +54,7 @@ export interface PipelineDeps {
|
|
|
53
54
|
* @returns Principal on success
|
|
54
55
|
* @throws On forge / expiry / revoke — pipeline maps to `Unauthorized`
|
|
55
56
|
*/
|
|
56
|
-
readonly verifyBearer?: (token: string) => Promise<ResolvedPrincipal>;
|
|
57
|
+
readonly verifyBearer?: (token: string, request?: Request) => Promise<ResolvedPrincipal>;
|
|
57
58
|
/**
|
|
58
59
|
* Optional token extraction (e.g. cookie → Bearer when cookies enabled).
|
|
59
60
|
* When omitted, only `Authorization: Bearer` is read.
|
|
@@ -155,6 +156,7 @@ export function applyPrincipal(bag: PrincipalBag, resolved: ResolvedPrincipal |
|
|
|
155
156
|
for (const s of resolved.scopes) bag.auth.scopes.add(s);
|
|
156
157
|
}
|
|
157
158
|
if (resolved.verified !== undefined) bag.auth.verified = resolved.verified;
|
|
159
|
+
bag.auth.apiKeyId = resolved.apiKeyId ?? null;
|
|
158
160
|
}
|
|
159
161
|
}
|
|
160
162
|
|
|
@@ -192,7 +194,7 @@ export function createElementPipelineHooks(deps: PipelineDeps): {
|
|
|
192
194
|
return fail("Unauthorized", {});
|
|
193
195
|
}
|
|
194
196
|
try {
|
|
195
|
-
const principal = await deps.verifyBearer(token);
|
|
197
|
+
const principal = await deps.verifyBearer(token, ctx.request);
|
|
196
198
|
applyPrincipal(deps.principals, principal);
|
|
197
199
|
if (principal.apiKeyId) {
|
|
198
200
|
deps.telemetry.dimensions.api_key = principal.apiKeyId;
|
|
@@ -32,7 +32,7 @@ describe("plugin element contributions", () => {
|
|
|
32
32
|
.table("otp_codes")
|
|
33
33
|
.needs("store.sql");
|
|
34
34
|
|
|
35
|
-
on(http.get("/ok").
|
|
35
|
+
on(http.get("/ok").public(), flow("ok", { do: () => ({ ok: true }) }));
|
|
36
36
|
|
|
37
37
|
const app = oke({
|
|
38
38
|
name: "plugin-elements",
|
|
@@ -75,7 +75,7 @@ describe("plugin .needs() resolution", () => {
|
|
|
75
75
|
test("boot fails when plugin needs missing peer plugin", async () => {
|
|
76
76
|
resetBindings();
|
|
77
77
|
resetFlowSeq();
|
|
78
|
-
on(http.get("/x").
|
|
78
|
+
on(http.get("/x").public(), flow("x", { do: () => ({ ok: true }) }));
|
|
79
79
|
const dependent = plugin("needs-auth", { version: "0.0.1" }).needs("auth");
|
|
80
80
|
const app = oke({ name: "needs-boot", gate: { policies: [gate.public] } }).plug(dependent);
|
|
81
81
|
await expect(app.boot({ env: "test" })).rejects.toThrow(PluginNeedsError);
|
package/src/kernel/ready.test.ts
CHANGED
|
@@ -38,7 +38,7 @@ describe("GET /_/ready", () => {
|
|
|
38
38
|
});
|
|
39
39
|
const bindings: Binding[] = [
|
|
40
40
|
{
|
|
41
|
-
trigger: http.post("/charge").
|
|
41
|
+
trigger: http.post("/charge").public(),
|
|
42
42
|
flow: charge as AnyFlowDef,
|
|
43
43
|
},
|
|
44
44
|
];
|
|
@@ -82,7 +82,7 @@ describe("GET /_oke/client.json", () => {
|
|
|
82
82
|
startScheduler: false,
|
|
83
83
|
env: "test",
|
|
84
84
|
gate: { unguardedHttp: "allow" },
|
|
85
|
-
bindings: [{ trigger: http.get("/ping").
|
|
85
|
+
bindings: [{ trigger: http.get("/ping").public(), flow: ping as AnyFlowDef }],
|
|
86
86
|
});
|
|
87
87
|
await app.boot();
|
|
88
88
|
const res = await app.fetch(new Request("http://127.0.0.1/_oke/client.json"));
|
|
@@ -63,6 +63,20 @@ describe("http.resource — gate / live", () => {
|
|
|
63
63
|
expect(remove?.isLive).toBe(false);
|
|
64
64
|
});
|
|
65
65
|
|
|
66
|
+
test(".public().live() stamps the sentinel on every verb", () => {
|
|
67
|
+
resetBindings();
|
|
68
|
+
resetFlowSeq();
|
|
69
|
+
on(http.resource("/notes", bag()).public().live());
|
|
70
|
+
expect(httpOf("/notes", "GET")?.gates.map((g) => (typeof g === "string" ? g : g.name))).toEqual(
|
|
71
|
+
["public"],
|
|
72
|
+
);
|
|
73
|
+
expect(
|
|
74
|
+
httpOf("/notes", "POST")?.gates.map((g) => (typeof g === "string" ? g : g.name)),
|
|
75
|
+
).toEqual(["public"]);
|
|
76
|
+
expect(httpOf("/notes", "GET")?.isLive).toBe(true);
|
|
77
|
+
expect(httpOf("/notes", "POST")?.isLive).toBe(false);
|
|
78
|
+
});
|
|
79
|
+
|
|
66
80
|
test(".live().gate(member) order does not matter", () => {
|
|
67
81
|
resetBindings();
|
|
68
82
|
resetFlowSeq();
|
|
@@ -23,7 +23,7 @@ describe("execute run duration", () => {
|
|
|
23
23
|
await runs.open();
|
|
24
24
|
|
|
25
25
|
on(
|
|
26
|
-
http.get("/ping").
|
|
26
|
+
http.get("/ping").public(),
|
|
27
27
|
flow("demo.ping", {
|
|
28
28
|
do: () => ({ ok: true }),
|
|
29
29
|
}),
|
|
@@ -68,7 +68,7 @@ describe("execute run duration", () => {
|
|
|
68
68
|
await runs.open();
|
|
69
69
|
try {
|
|
70
70
|
on(
|
|
71
|
-
http.get("/ping").
|
|
71
|
+
http.get("/ping").public(),
|
|
72
72
|
flow("demo.ping", {
|
|
73
73
|
do: () => ({ ok: true }),
|
|
74
74
|
}),
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* In-place path / name stamps and fail-loud HTTP boot.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, expect, test, beforeEach } from "bun:test";
|
|
6
|
+
import type { RuntimeRouteMap } from "./adopt-routes.ts";
|
|
7
|
+
import { oke } from "./app.ts";
|
|
8
|
+
import { flow, resetFlowSeq } from "./flow.ts";
|
|
9
|
+
import { resetRegisteredFlowUnits, registerFlowUnits } from "./flow-units.ts";
|
|
10
|
+
import { listBindings, on, resetBindings } from "./on.ts";
|
|
11
|
+
import { stampFlowName, stampHttpPath } from "./stamp-http.ts";
|
|
12
|
+
import { HTTP_PATH_PENDING } from "./http-path-pending.ts";
|
|
13
|
+
import { http } from "./triggers.ts";
|
|
14
|
+
|
|
15
|
+
beforeEach(() => {
|
|
16
|
+
resetBindings();
|
|
17
|
+
resetFlowSeq();
|
|
18
|
+
resetRegisteredFlowUnits();
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
describe("stampHttpPath", () => {
|
|
22
|
+
test("mutates the on() binding in place — listBindings sees the stamped path", () => {
|
|
23
|
+
const f = on(http.get().public(), flow({ do: () => ({ ok: true }) }));
|
|
24
|
+
expect(f.$trigger && f.$trigger.kind === "http" && f.$trigger.path).toBe(HTTP_PATH_PENDING);
|
|
25
|
+
stampHttpPath(f, "/notes/:id");
|
|
26
|
+
expect(f.$trigger && f.$trigger.kind === "http" && f.$trigger.path).toBe("/notes/:id");
|
|
27
|
+
const bound = listBindings()[0];
|
|
28
|
+
expect(bound?.trigger.kind === "http" && bound.trigger.path).toBe("/notes/:id");
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test("explicit http.get('/x') wins — stamp does not overwrite", () => {
|
|
32
|
+
const f = on(http.get("/me/tasks").public(), flow("notes.get", { do: () => 1 }));
|
|
33
|
+
stampHttpPath(f, "/notes/:id");
|
|
34
|
+
expect(f.$trigger && f.$trigger.kind === "http" && f.$trigger.path).toBe("/me/tasks");
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
describe("stampFlowName", () => {
|
|
39
|
+
test("fills a nameless flow as unit.export", () => {
|
|
40
|
+
const f = flow({ do: () => 1 });
|
|
41
|
+
expect(f.name).toBe("");
|
|
42
|
+
stampFlowName(f, "notes.get");
|
|
43
|
+
expect(f.name).toBe("notes.get");
|
|
44
|
+
expect(f.unit).toBe("notes");
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test("explicit flow('notes.get') wins", () => {
|
|
48
|
+
const f = flow("notes.get", { do: () => 1 });
|
|
49
|
+
stampFlowName(f, "notes.list");
|
|
50
|
+
expect(f.name).toBe("notes.get");
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
describe("oke — unresolved sentinel and duplicate routes", () => {
|
|
55
|
+
test("unresolved http.get() fails construction", () => {
|
|
56
|
+
on(http.get().public(), flow("notes.get", { do: () => 1 }));
|
|
57
|
+
expect(() => oke({ name: "t", autoBoot: false })).toThrow(/OKE1010/);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test("duplicate GET /notes fails construction", () => {
|
|
61
|
+
on(http.get("/notes").public(), flow("notes.list", { do: () => 1 }));
|
|
62
|
+
on(http.get("/notes").public(), flow("notes.also", { do: () => 1 }));
|
|
63
|
+
expect(() => oke({ name: "t", autoBoot: false })).toThrow(/OKE1011/);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test("nameless HTTP flow fails construction", () => {
|
|
67
|
+
on(http.get("/notes").public(), flow({ do: () => 1 }));
|
|
68
|
+
expect(() => oke({ name: "t", autoBoot: false })).toThrow(/OKE1012/);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test("registerFlowUnits drains into $routes; registry ignore does not", () => {
|
|
72
|
+
const get = on(http.get("/notes/:id").public(), flow("notes.get", { do: () => 1 }));
|
|
73
|
+
registerFlowUnits({ notes: { get } });
|
|
74
|
+
const ignored = oke({ name: "t", autoBoot: false, registry: "ignore" });
|
|
75
|
+
expect(ignored.$routes).toEqual({});
|
|
76
|
+
const app = oke({ name: "t", autoBoot: false });
|
|
77
|
+
expect((app.$routes as RuntimeRouteMap).notes?.get?.path).toBe("/notes/:id");
|
|
78
|
+
});
|
|
79
|
+
});
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* In-place HTTP path + Flow name stamps.
|
|
3
|
+
*
|
|
4
|
+
* `on()` already registered the trigger (possibly still on the pending-path
|
|
5
|
+
* sentinel). A wrapper that returned a new trigger would leave `listBindings()`
|
|
6
|
+
* on the sentinel — mutate the existing object.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { isFlow, type AnyFlowDef, type FlowDef } from "./flow.ts";
|
|
10
|
+
import { isPendingHttpPath, type HttpPathPending } from "./http-path-pending.ts";
|
|
11
|
+
import type { HttpTrigger, Trigger } from "./triggers.ts";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Replace a pending HTTP path on {@link FlowDef} with the file-tree stamp.
|
|
15
|
+
*
|
|
16
|
+
* Explicit `http.get("/x")` wins — the pending sentinel is the only path
|
|
17
|
+
* this function overwrites. Returns the same object with a tighter trigger
|
|
18
|
+
* type when the original path was pending.
|
|
19
|
+
*
|
|
20
|
+
* @param flow - Bound flow (already passed through `on()`)
|
|
21
|
+
* @param path - Inferred URL (`/notes/:id`)
|
|
22
|
+
*/
|
|
23
|
+
export function stampHttpPath<F extends AnyFlowDef, P extends string>(
|
|
24
|
+
flow: F,
|
|
25
|
+
path: P,
|
|
26
|
+
): StampHttpPath<F, P> {
|
|
27
|
+
if (!isFlow(flow)) return flow as StampHttpPath<F, P>;
|
|
28
|
+
const apply = (trigger: Trigger): void => {
|
|
29
|
+
if (trigger.kind !== "http") return;
|
|
30
|
+
if (!isPendingHttpPath(trigger.path)) return;
|
|
31
|
+
(trigger as { path: string }).path = path;
|
|
32
|
+
};
|
|
33
|
+
if (flow.$trigger) apply(flow.$trigger);
|
|
34
|
+
for (const trigger of flow.triggers) apply(trigger);
|
|
35
|
+
return flow as StampHttpPath<F, P>;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Stamp `unit.export` onto a nameless (or `flow_*`) Flow.
|
|
40
|
+
*
|
|
41
|
+
* Explicit `flow("notes.get")` wins. Fills `unit` when missing.
|
|
42
|
+
*
|
|
43
|
+
* @param flow - Flow definition
|
|
44
|
+
* @param name - `notes.get`
|
|
45
|
+
*/
|
|
46
|
+
export function stampFlowName<F extends AnyFlowDef>(flow: F, name: string): F {
|
|
47
|
+
if (!isFlow(flow)) return flow;
|
|
48
|
+
const f = flow as { name: string; unit: string | undefined };
|
|
49
|
+
if (!f.name || f.name.startsWith("flow_")) {
|
|
50
|
+
f.name = name;
|
|
51
|
+
}
|
|
52
|
+
if (!f.unit) {
|
|
53
|
+
const resolved = f.name || name;
|
|
54
|
+
const dot = resolved.indexOf(".");
|
|
55
|
+
f.unit = dot > 0 ? resolved.slice(0, dot) : resolved;
|
|
56
|
+
}
|
|
57
|
+
return flow;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Type-level path replacement — only when the bound trigger is still pending.
|
|
62
|
+
*
|
|
63
|
+
* @typeParam F - Flow definition
|
|
64
|
+
* @typeParam P - Stamped path
|
|
65
|
+
*/
|
|
66
|
+
export type StampHttpPath<F, P extends string> =
|
|
67
|
+
F extends FlowDef<infer I, infer O, infer E, infer D, infer T>
|
|
68
|
+
? T extends HttpTrigger<infer M, infer Cur>
|
|
69
|
+
? [Cur] extends [HttpPathPending]
|
|
70
|
+
? FlowDef<I, O, E, D, HttpTrigger<M, P>>
|
|
71
|
+
: F
|
|
72
|
+
: F
|
|
73
|
+
: F;
|
|
74
|
+
|
|
75
|
+
export { HTTP_PATH_PENDING, isPendingHttpPath } from "./http-path-pending.ts";
|