okengine 0.21.1 → 0.22.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/AGENTS.md +4 -4
- package/LICENSE +202 -0
- package/NOTICE +2 -0
- package/README.md +15 -9
- package/manifest.v1.schema.json +10 -0
- package/package.json +22 -20
- package/site/content/docs/ai/skills.mdx +1 -1
- package/site/content/docs/client/calling.mdx +27 -19
- package/site/content/docs/client/index.mdx +1 -1
- package/site/content/docs/elements/channel/email.mdx +1 -1
- package/site/content/docs/elements/channel/index.mdx +1 -1
- package/site/content/docs/elements/flow/http.mdx +4 -4
- package/site/content/docs/elements/flow/index.mdx +3 -3
- package/site/content/docs/elements/flow/meta.json +1 -1
- package/site/content/docs/elements/gate/tenancy.mdx +1 -1
- package/site/content/docs/elements/vault/rotation.mdx +15 -0
- package/site/content/docs/plugins/otp.mdx +31 -31
- package/site/content/docs/recipes/mailpit.mdx +1 -1
- package/site/content/docs/recipes/meilisearch.mdx +1 -1
- package/site/content/docs/recipes/pgdog.mdx +1 -1
- package/site/content/docs/reference/configuration.mdx +2 -2
- package/site/content/docs/reference/errors.mdx +12 -2
- package/site/content/docs/reference/fx.mdx +28 -15
- package/site/content/docs/reference/idempotency.mdx +192 -0
- package/site/content/docs/reference/index.mdx +6 -1
- package/site/content/docs/reference/meta.json +2 -1
- package/site/content/docs/understand/meta.json +1 -1
- package/site/content/docs/{elements/flow → understand}/routing.mdx +11 -10
- package/site/content/docs/understand/the-architecture.mdx +14 -12
- package/site/content/docs/understand/try-it.mdx +3 -3
- package/src/cli/competitor-mention-removal.test.ts +3 -3
- package/src/cli/docker-cli.test.ts +1 -1
- package/src/cli/index.ts +9 -1
- package/src/cli/load-config.images.test.ts +10 -10
- package/src/cli/load-config.ts +1 -1
- package/src/cli/vault-cmd.test.ts +23 -0
- package/src/cli/vault-cmd.ts +7 -0
- package/src/client/create.ts +18 -1
- package/src/client/transport.test.ts +225 -0
- package/src/client/transport.ts +132 -19
- package/src/client/types.ts +29 -3
- package/src/compiler/effects-infer.ts +726 -16
- package/src/compiler/extract.test.ts +37 -0
- package/src/compiler/extract.ts +113 -12
- package/src/compiler/fixtures/skyport.expected.json +24 -0
- package/src/compiler/fx-follow.test.ts +164 -0
- package/src/compiler/fx-index.ts +399 -0
- package/src/console/server/flows-invoke.test.ts +2 -2
- package/src/console/ui-next/dist/assets/{access-page-Bgt9bq2r.js → access-page-Ze6ckjdd.js} +2 -2
- package/src/console/ui-next/dist/assets/agent-disclosure-Bkohc_8X.js +1 -0
- package/src/console/ui-next/dist/assets/{cache-glyph-92uM5MO7.js → cache-glyph-rL0lHxat.js} +1 -1
- package/src/console/ui-next/dist/assets/{call-pii-button-DtGVPtcs.js → call-pii-button-Deo4PP18.js} +1 -1
- package/src/console/ui-next/dist/assets/{collapsible-DN7l6zmC.js → collapsible-CoJ6amHf.js} +1 -1
- package/src/console/ui-next/dist/assets/{copy-inline-button-CAYD18cr.js → copy-inline-button-CWG5_ZOh.js} +1 -1
- package/src/console/ui-next/dist/assets/{detail-header-DVWNjWjg.js → detail-header-b_ZzIsmU.js} +1 -1
- package/src/console/ui-next/dist/assets/{dropdown-menu-4h2LOVXM.js → dropdown-menu-nwXEO1Ac.js} +1 -1
- package/src/console/ui-next/dist/assets/duration-tone-BbQ_8z50.js +9 -0
- package/src/console/ui-next/dist/assets/element-icons-CwyLpVXz.js +1 -0
- package/src/console/ui-next/dist/assets/{explorer-empty-CJs5A-wm.js → explorer-empty-C8sSCqYT.js} +1 -1
- package/src/console/ui-next/dist/assets/{flows-page-BMHs-IzK.js → flows-page-D2E7BtKK.js} +1 -1
- package/src/console/ui-next/dist/assets/{highlighted-json-BlAEVgNW.js → highlighted-json-CPYBzDh0.js} +1 -1
- package/src/console/ui-next/dist/assets/{http-method-BDf7OAHv.js → http-method-DTPevKmM.js} +1 -1
- package/src/console/ui-next/dist/assets/index-vTuwmeQz.js +58 -0
- package/src/console/ui-next/dist/assets/observability-page-SJTSzHFJ.js +4 -0
- package/src/console/ui-next/dist/assets/{replica-lag-B3GLNVfF.js → replica-lag-BM27lXld.js} +3 -3
- package/src/console/ui-next/dist/assets/{request-meta-DMbnAe3f.js → request-meta-BjFT7DNl.js} +1 -1
- package/src/console/ui-next/dist/assets/shortcut-keys-CUNegEV4.js +1 -0
- package/src/console/ui-next/dist/assets/store-page-Qj9ThRE-.js +41 -0
- package/src/console/ui-next/dist/assets/{trace-detail-sheet-Htk8Cm9t.js → trace-detail-sheet-Cmitq3da.js} +2 -2
- package/src/console/ui-next/dist/assets/{tree-expand-toggle-CFMPWX4f.js → tree-expand-toggle-CbDIB-7x.js} +2 -2
- package/src/console/ui-next/dist/assets/units-page-_PuYqFty.js +1 -0
- package/src/console/ui-next/dist/assets/{use-vault-list-CT4-gajj.js → use-vault-list-Cs45Czkt.js} +1 -1
- package/src/console/ui-next/dist/assets/{vault-page-CBYl0LW_.js → vault-page-BOMr0go9.js} +2 -2
- package/src/console/ui-next/dist/index.html +2 -2
- package/src/console/ui-next/src/features/units/detail/flow-contract-panel.tsx +9 -0
- package/src/docker/docker.test.ts +13 -13
- package/src/docker/images-config.test.ts +12 -12
- package/src/docker/stack-id.test.ts +1 -1
- package/src/drivers/journal-postgres.ts +168 -3
- package/src/drivers/meilisearch.ts +2 -0
- package/src/drivers/vault-builtin.test.ts +10 -0
- package/src/drivers/vault-builtin.ts +13 -1
- package/src/drivers/vault-remote-bag.ts +5 -1
- package/src/drivers/vault-types.ts +6 -1
- package/src/elements/store/emit-drizzle.ts +2 -2
- package/src/elements/store/live-default.test.ts +2 -0
- package/src/elements/store/live-http.test.ts +1 -0
- package/src/elements/store/resource-list-docs.test.ts +2 -2
- package/src/elements/store/resource.test.ts +3 -3
- package/src/elements/store/schema-decl.test.ts +1 -0
- package/src/elements/store/sql-condition.ts +24 -7
- package/src/elements/store/upsert-app.test.ts +1 -1
- package/src/elements/vault/audit.test.ts +137 -0
- package/src/elements/vault/audit.ts +106 -13
- package/src/elements/vault/boot-chain.ts +9 -1
- package/src/elements/vault/builtin-adapter.ts +33 -3
- package/src/i18n/catalogs/ar.ts +4 -0
- package/src/i18n/catalogs/en.ts +4 -0
- package/src/kernel/abort-scope.ts +33 -2
- package/src/kernel/app.ts +191 -18
- package/src/kernel/auto-cache.test.ts +6 -6
- package/src/kernel/boot-bind/vault.ts +1 -0
- package/src/kernel/boot.ts +19 -1
- package/src/kernel/builtin-errors.ts +8 -0
- package/src/kernel/client-descriptor.ts +1 -0
- package/src/kernel/concurrency.test.ts +33 -0
- package/src/kernel/effects-stamping.test.ts +2 -2
- package/src/kernel/errors-compiler.ts +20 -0
- package/src/kernel/external-effects.test.ts +36 -0
- package/src/kernel/flow.ts +24 -0
- package/src/kernel/fx-fetch.ts +5 -1
- package/src/kernel/fx.ts +5 -1
- package/src/kernel/idempotency-store.ts +578 -0
- package/src/kernel/idempotency.test.ts +556 -0
- package/src/kernel/idempotency.ts +304 -0
- package/src/kernel/index.ts +1 -0
- package/src/kernel/journal.ts +56 -0
- package/src/manifest/types.ts +8 -0
- package/src/plugins/auth-delivery.mailpit.integration.test.ts +1 -1
- package/src/plugins/otp.test.ts +65 -1
- package/src/plugins/otp.ts +59 -7
- package/src/release/absolute-regression.test.ts +63 -0
- package/src/release/http-graph.test.ts +25 -0
- package/src/release/http-graph.ts +90 -0
- package/src/release/index.ts +3 -0
- package/src/release/limits.ts +17 -2
- package/src/release/measure.ts +105 -14
- package/src/test/create-test-app.test.ts +3 -1
- package/src/test/create-test-app.ts +40 -0
- package/src/test/live-signals.test.ts +1 -0
- package/src/test/provisions.integration.test.ts +1 -0
- package/src/test/tenant-isolation.test.ts +1 -0
- package/src/console/ui-next/dist/assets/agent-disclosure-B43CXZZR.js +0 -1
- package/src/console/ui-next/dist/assets/duration-tone-BmIR9FV8.js +0 -9
- package/src/console/ui-next/dist/assets/element-icons-BI8cJgdh.js +0 -1
- package/src/console/ui-next/dist/assets/index-BKpaes3n.js +0 -63
- package/src/console/ui-next/dist/assets/observability-page-WnVLI-0j.js +0 -4
- package/src/console/ui-next/dist/assets/shortcut-keys-3ILd8oGn.js +0 -1
- package/src/console/ui-next/dist/assets/store-page-KvFDingJ.js +0 -41
- package/src/console/ui-next/dist/assets/units-page-C4NdNuxP.js +0 -1
|
@@ -9,8 +9,10 @@
|
|
|
9
9
|
|
|
10
10
|
import { VaultError } from "./errors.ts";
|
|
11
11
|
import type { VaultErrorCode } from "./errors.ts";
|
|
12
|
-
import type { VaultActorType } from "./types.ts";
|
|
13
|
-
|
|
12
|
+
import type { VaultActorType, VaultAuditConfig, VaultAuditSinkKind } from "./types.ts";
|
|
13
|
+
|
|
14
|
+
/** Bound on a webhook audit POST so a dead receiver cannot stall a write. */
|
|
15
|
+
const AUDIT_WEBHOOK_TIMEOUT_MS = 5_000;
|
|
14
16
|
|
|
15
17
|
/** Operations recorded in the audit chain. */
|
|
16
18
|
export type AuditAction =
|
|
@@ -110,8 +112,10 @@ export interface AuditWriter {
|
|
|
110
112
|
export interface CreateAuditSinkOptions {
|
|
111
113
|
/** Required for the `db` sink. */
|
|
112
114
|
readonly writer?: AuditWriter;
|
|
113
|
-
/** Required for the `webhook` sink
|
|
115
|
+
/** Required for the `webhook` sink. Absolute `http:` or `https:` URL. */
|
|
114
116
|
readonly webhookUrl?: string;
|
|
117
|
+
/** Fetch implementation for the `webhook` sink (tests). */
|
|
118
|
+
readonly fetch?: typeof globalThis.fetch;
|
|
115
119
|
/** Line writer for the `stdout` sink (tests). */
|
|
116
120
|
readonly write?: (line: string) => void;
|
|
117
121
|
/** Clock for `stdout` timestamps (tests). */
|
|
@@ -169,23 +173,98 @@ export async function computeAuditRowHash(
|
|
|
169
173
|
return Buffer.from(new Uint8Array(digest)).toString("hex");
|
|
170
174
|
}
|
|
171
175
|
|
|
176
|
+
/**
|
|
177
|
+
* Reject a webhook URL that is missing or not an absolute http(s) URL.
|
|
178
|
+
*
|
|
179
|
+
* @param webhookUrl - Configured destination
|
|
180
|
+
* @returns Absolute URL string
|
|
181
|
+
* @throws VaultError `MISSING_PEER` when the URL is absent or not http(s)
|
|
182
|
+
*/
|
|
183
|
+
function requireWebhookUrl(webhookUrl: string | undefined): string {
|
|
184
|
+
const raw = webhookUrl?.trim() ?? "";
|
|
185
|
+
if (raw.length === 0) {
|
|
186
|
+
throw new VaultError("MISSING_PEER", "vault: webhook audit sink requires webhookUrl");
|
|
187
|
+
}
|
|
188
|
+
let parsed: URL;
|
|
189
|
+
try {
|
|
190
|
+
parsed = new URL(raw);
|
|
191
|
+
} catch {
|
|
192
|
+
throw new VaultError(
|
|
193
|
+
"MISSING_PEER",
|
|
194
|
+
"vault: webhook audit sink requires an absolute http(s) webhookUrl",
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
|
198
|
+
throw new VaultError(
|
|
199
|
+
"MISSING_PEER",
|
|
200
|
+
"vault: webhook audit sink requires an absolute http(s) webhookUrl",
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
return parsed.href;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* JSON body shared by the `stdout` and `webhook` sinks.
|
|
208
|
+
*
|
|
209
|
+
* Only normalized metadata is serialized — an entry never holds a value.
|
|
210
|
+
*
|
|
211
|
+
* @param entry - Operation to record
|
|
212
|
+
* @param now - Clock when the entry has no `at`
|
|
213
|
+
*/
|
|
214
|
+
function auditEventJson(entry: AuditEntry, now: () => Date): string {
|
|
215
|
+
return JSON.stringify({
|
|
216
|
+
sink: "oke.vault.audit",
|
|
217
|
+
...toAuditHashPayload(entry, entry.at ?? now()),
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
|
|
172
221
|
/**
|
|
173
222
|
* Build an audit sink.
|
|
174
223
|
*
|
|
175
224
|
* - `db` delegates to an {@link AuditWriter} supplied by storage.
|
|
176
225
|
* - `stdout` writes one secret-free JSON line per operation.
|
|
177
|
-
* - `webhook`
|
|
226
|
+
* - `webhook` POSTs that same JSON. Delivery failure throws; callers that
|
|
227
|
+
* record after a successful operation swallow it.
|
|
228
|
+
*
|
|
229
|
+
* Hash-chain verify and purge apply only to the `db` sink.
|
|
178
230
|
*
|
|
179
231
|
* @param kind - Sink kind from `audit.sink`
|
|
180
232
|
* @param opts - Writer / webhook URL / test seams
|
|
181
|
-
* @throws VaultError `
|
|
233
|
+
* @throws VaultError `MISSING_PEER` when `db` has no writer or `webhook` has no absolute URL
|
|
182
234
|
*/
|
|
183
235
|
export function createAuditSink(
|
|
184
236
|
kind: VaultAuditSinkKind,
|
|
185
237
|
opts: CreateAuditSinkOptions = {},
|
|
186
238
|
): AuditSink {
|
|
187
239
|
if (kind === "webhook") {
|
|
188
|
-
|
|
240
|
+
const url = requireWebhookUrl(opts.webhookUrl);
|
|
241
|
+
const fetchFn = opts.fetch ?? globalThis.fetch;
|
|
242
|
+
const now = opts.now ?? (() => new Date());
|
|
243
|
+
return {
|
|
244
|
+
kind: "webhook",
|
|
245
|
+
async append(entry) {
|
|
246
|
+
const body = auditEventJson(entry, now);
|
|
247
|
+
let response: Response;
|
|
248
|
+
try {
|
|
249
|
+
response = await fetchFn(url, {
|
|
250
|
+
method: "POST",
|
|
251
|
+
headers: { "content-type": "application/json" },
|
|
252
|
+
body,
|
|
253
|
+
redirect: "error",
|
|
254
|
+
signal: AbortSignal.timeout(AUDIT_WEBHOOK_TIMEOUT_MS),
|
|
255
|
+
});
|
|
256
|
+
} catch (err) {
|
|
257
|
+
if (err instanceof VaultError) throw err;
|
|
258
|
+
throw new VaultError("BACKEND_ERROR", "vault: webhook audit sink request failed");
|
|
259
|
+
}
|
|
260
|
+
if (!response.ok) {
|
|
261
|
+
throw new VaultError(
|
|
262
|
+
"BACKEND_ERROR",
|
|
263
|
+
`vault: webhook audit sink failed (${response.status})`,
|
|
264
|
+
);
|
|
265
|
+
}
|
|
266
|
+
},
|
|
267
|
+
};
|
|
189
268
|
}
|
|
190
269
|
|
|
191
270
|
if (kind === "db") {
|
|
@@ -206,17 +285,31 @@ export function createAuditSink(
|
|
|
206
285
|
return {
|
|
207
286
|
kind: "stdout",
|
|
208
287
|
async append(entry) {
|
|
209
|
-
|
|
210
|
-
write(
|
|
211
|
-
JSON.stringify({
|
|
212
|
-
sink: "oke.vault.audit",
|
|
213
|
-
...toAuditHashPayload(entry, entry.at ?? now()),
|
|
214
|
-
}),
|
|
215
|
-
);
|
|
288
|
+
write(auditEventJson(entry, now));
|
|
216
289
|
},
|
|
217
290
|
};
|
|
218
291
|
}
|
|
219
292
|
|
|
293
|
+
/**
|
|
294
|
+
* Pick the sink for a built-in adapter from `vault.audit`.
|
|
295
|
+
*
|
|
296
|
+
* `enabled: false` drops every row. Omitted config is the `db` chain.
|
|
297
|
+
*
|
|
298
|
+
* @param config - `vault.audit` block, when present
|
|
299
|
+
* @param opts - SQL writer and optional fetch
|
|
300
|
+
*/
|
|
301
|
+
export function auditSinkForConfig(
|
|
302
|
+
config: VaultAuditConfig | undefined,
|
|
303
|
+
opts: { readonly writer: AuditWriter; readonly fetch?: typeof globalThis.fetch },
|
|
304
|
+
): AuditSink {
|
|
305
|
+
if (config?.enabled === false) return createNullAuditSink();
|
|
306
|
+
return createAuditSink(config?.sink ?? "db", {
|
|
307
|
+
writer: opts.writer,
|
|
308
|
+
...(config?.webhookUrl === undefined ? {} : { webhookUrl: config.webhookUrl }),
|
|
309
|
+
...(opts.fetch === undefined ? {} : { fetch: opts.fetch }),
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
|
|
220
313
|
/**
|
|
221
314
|
* Sink that drops every entry (`audit.enabled: false`).
|
|
222
315
|
*
|
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
import { parseDotenv } from "../../drivers/vault-dotenv-parse.ts";
|
|
23
23
|
import type { VaultDriverId } from "../../drivers/vault-types.ts";
|
|
24
24
|
import { resolveComposeEnvPath } from "./chain.ts";
|
|
25
|
+
import type { VaultAuditConfig } from "./types.ts";
|
|
25
26
|
import type { VaultChainLayer } from "./runtime.ts";
|
|
26
27
|
|
|
27
28
|
/** Options for {@link buildVaultBootChain}. */
|
|
@@ -34,6 +35,10 @@ export interface BuildVaultBootChainOptions {
|
|
|
34
35
|
readonly env?: ConfigEnv;
|
|
35
36
|
/** Seed secrets for the terminal memory / managed layer. */
|
|
36
37
|
readonly seed?: Readonly<Record<string, string>>;
|
|
38
|
+
/**
|
|
39
|
+
* Built-in store audit block. Forwarded only to the `vault` driver layer.
|
|
40
|
+
*/
|
|
41
|
+
readonly audit?: VaultAuditConfig;
|
|
37
42
|
}
|
|
38
43
|
|
|
39
44
|
/**
|
|
@@ -142,7 +147,10 @@ export function buildVaultBootChain(options: BuildVaultBootChainOptions): VaultC
|
|
|
142
147
|
{
|
|
143
148
|
driver: builtinVaultDriver,
|
|
144
149
|
source: "driver",
|
|
145
|
-
options: {
|
|
150
|
+
options: {
|
|
151
|
+
secrets: seed,
|
|
152
|
+
...(options.audit === undefined ? {} : { audit: options.audit }),
|
|
153
|
+
},
|
|
146
154
|
},
|
|
147
155
|
...envLayers,
|
|
148
156
|
];
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
* cleartext value ever reaches the log, an error message, or a `VaultError`.
|
|
21
21
|
*/
|
|
22
22
|
|
|
23
|
-
import {
|
|
23
|
+
import { auditSinkForConfig, type AuditAction, type AuditSink } from "./audit.ts";
|
|
24
24
|
import {
|
|
25
25
|
ALGORITHM,
|
|
26
26
|
buildAad,
|
|
@@ -66,6 +66,7 @@ import type {
|
|
|
66
66
|
VaultActor,
|
|
67
67
|
VaultAdapter,
|
|
68
68
|
VaultAlgorithm,
|
|
69
|
+
VaultAuditConfig,
|
|
69
70
|
VaultGetOptions,
|
|
70
71
|
VaultInitResult,
|
|
71
72
|
VaultListEntry,
|
|
@@ -96,8 +97,15 @@ export interface CreateBuiltinVaultOptions {
|
|
|
96
97
|
* {@link VaultAdapter.unseal} is not required.
|
|
97
98
|
*/
|
|
98
99
|
readonly unsealer?: Unsealer;
|
|
99
|
-
/** Audit destination.
|
|
100
|
+
/** Audit destination. Wins over {@link audit} when both are set. */
|
|
100
101
|
readonly auditSink?: AuditSink;
|
|
102
|
+
/**
|
|
103
|
+
* `vault.audit` from config. Omitted means the `db` hash chain.
|
|
104
|
+
* Ignored when {@link auditSink} is set.
|
|
105
|
+
*/
|
|
106
|
+
readonly audit?: VaultAuditConfig;
|
|
107
|
+
/** Fetch override for the `webhook` audit sink (tests). */
|
|
108
|
+
readonly fetch?: typeof globalThis.fetch;
|
|
101
109
|
/** DEKs re-wrapped per rotation batch. Defaults to {@link DEFAULT_KEK_REWRAP_BATCH_SIZE}. */
|
|
102
110
|
readonly kekRewrapBatchSize?: number;
|
|
103
111
|
/**
|
|
@@ -306,7 +314,25 @@ export function createBuiltinVaultAdapter(opts: CreateBuiltinVaultOptions): Buil
|
|
|
306
314
|
const db = createResilientSqlExec(rawDb);
|
|
307
315
|
const batchSize = Math.max(1, opts.kekRewrapBatchSize ?? DEFAULT_KEK_REWRAP_BATCH_SIZE);
|
|
308
316
|
const rotateLeaseMs = Math.max(50, opts.rotateLeaseMs ?? DEFAULT_ROTATE_LEASE_MS);
|
|
309
|
-
const audit =
|
|
317
|
+
const audit =
|
|
318
|
+
opts.auditSink ??
|
|
319
|
+
auditSinkForConfig(opts.audit, {
|
|
320
|
+
writer: createSqlAuditWriter(rawDb),
|
|
321
|
+
...(opts.fetch === undefined ? {} : { fetch: opts.fetch }),
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* Hash-chain commands only exist for the `db` sink.
|
|
326
|
+
*
|
|
327
|
+
* @param op - Command name for the error
|
|
328
|
+
*/
|
|
329
|
+
function requireDbAudit(op: string): void {
|
|
330
|
+
if (audit.kind === "db") return;
|
|
331
|
+
throw new VaultError(
|
|
332
|
+
"UNSUPPORTED",
|
|
333
|
+
`vault: ${op} requires audit.sink "db" (got "${audit.kind}")`,
|
|
334
|
+
);
|
|
335
|
+
}
|
|
310
336
|
|
|
311
337
|
let unsealer: Unsealer | null = opts.unsealer ?? null;
|
|
312
338
|
let pending: PendingRotation | null = null;
|
|
@@ -1018,6 +1044,7 @@ export function createBuiltinVaultAdapter(opts: CreateBuiltinVaultOptions): Buil
|
|
|
1018
1044
|
},
|
|
1019
1045
|
|
|
1020
1046
|
async purgeAuditBefore(before: Date): Promise<number> {
|
|
1047
|
+
requireDbAudit("audit purge");
|
|
1021
1048
|
await ensureReady();
|
|
1022
1049
|
const removed = await purgeAuditRows(db, before);
|
|
1023
1050
|
await record("purge", undefined);
|
|
@@ -1043,16 +1070,19 @@ export function createBuiltinVaultAdapter(opts: CreateBuiltinVaultOptions): Buil
|
|
|
1043
1070
|
},
|
|
1044
1071
|
|
|
1045
1072
|
async verifyAudit(): Promise<AuditChainResult> {
|
|
1073
|
+
requireDbAudit("audit verify");
|
|
1046
1074
|
await ensureReady();
|
|
1047
1075
|
return verifyAuditChain(db);
|
|
1048
1076
|
},
|
|
1049
1077
|
|
|
1050
1078
|
async listAudit(options?: AuditPageOptions): Promise<readonly VaultAuditRecord[]> {
|
|
1079
|
+
requireDbAudit("audit list");
|
|
1051
1080
|
await ensureReady();
|
|
1052
1081
|
return readAuditPage(db, options ?? {});
|
|
1053
1082
|
},
|
|
1054
1083
|
|
|
1055
1084
|
async readAuditRow(id: string): Promise<VaultAuditRecord | null> {
|
|
1085
|
+
requireDbAudit("audit read");
|
|
1056
1086
|
await ensureReady();
|
|
1057
1087
|
return readAuditRow(db, id);
|
|
1058
1088
|
},
|
package/src/i18n/catalogs/ar.ts
CHANGED
|
@@ -86,6 +86,10 @@ export const builtinAr = {
|
|
|
86
86
|
"DatabaseError.retryable": "قاعدة البيانات مشغولة. حاول بعد قليل.",
|
|
87
87
|
ServiceUnavailable: "الخدمة غير متاحة مؤقتاً.",
|
|
88
88
|
InternalError: "حدث خطأ. حاول مرة أخرى.",
|
|
89
|
+
IdempotencyKeyMissing: "هذا الاستدعاء يتطلب ترويسة Idempotency-Key.",
|
|
90
|
+
IdempotencyKeyInvalid: "يجب أن يكون Idempotency-Key من 16 إلى 255 حرفًا ASCII قابلًا للطباعة.",
|
|
91
|
+
IdempotencyKeyReused: "استُخدم Idempotency-Key هذا من قبل مع طلب مختلف.",
|
|
92
|
+
IdempotencyInProgress: "ما زال Idempotency-Key هذا قيد التنفيذ. أعد المحاولة بعد المهلة.",
|
|
89
93
|
InvalidQuery: "الاستعلام غير صالح.",
|
|
90
94
|
UnsupportedMediaType: "نوع المحتوى هذا غير مدعوم.",
|
|
91
95
|
AuthFailed: "فشلت المصادقة.",
|
package/src/i18n/catalogs/en.ts
CHANGED
|
@@ -86,6 +86,10 @@ export const builtinEn = {
|
|
|
86
86
|
"DatabaseError.retryable": "The database is busy. Try again shortly.",
|
|
87
87
|
ServiceUnavailable: "The service is temporarily unavailable.",
|
|
88
88
|
InternalError: "Something went wrong. Try again.",
|
|
89
|
+
IdempotencyKeyMissing: "This call requires an Idempotency-Key header.",
|
|
90
|
+
IdempotencyKeyInvalid: "Idempotency-Key must be 16–255 printable ASCII characters.",
|
|
91
|
+
IdempotencyKeyReused: "This Idempotency-Key was already used with a different request.",
|
|
92
|
+
IdempotencyInProgress: "This Idempotency-Key is still running. Retry after the given delay.",
|
|
89
93
|
InvalidQuery: "The query is not valid.",
|
|
90
94
|
UnsupportedMediaType: "That content type is not supported.",
|
|
91
95
|
AuthFailed: "Authentication failed.",
|
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
* Ambient AbortSignal scope for structured concurrency.
|
|
3
3
|
*
|
|
4
4
|
* Same ALS pattern as dry-run: `fx.all` / `fx.race` enter a child signal so
|
|
5
|
-
* cooperative branches
|
|
6
|
-
*
|
|
5
|
+
* cooperative branches and outbound HTTP can observe cancellation without
|
|
6
|
+
* every call site passing a signal argument.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
@@ -34,6 +34,21 @@ export async function withAbortSignal<T>(
|
|
|
34
34
|
return storage.run(signal, fn);
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
+
/** Never-aborted signal for work that must outlive the caller connection. */
|
|
38
|
+
const DETACHED: AbortSignal = new AbortController().signal;
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Run `fn` without the request abort signal.
|
|
42
|
+
*
|
|
43
|
+
* A claimed idempotent `do` keeps running when the client times out. The
|
|
44
|
+
* lease, not the socket, is what detects a crash.
|
|
45
|
+
*
|
|
46
|
+
* @param fn - Work to detach
|
|
47
|
+
*/
|
|
48
|
+
export function runDetached<T>(fn: () => T | Promise<T>): Promise<T> {
|
|
49
|
+
return withAbortSignal(DETACHED, fn);
|
|
50
|
+
}
|
|
51
|
+
|
|
37
52
|
/**
|
|
38
53
|
* When `parent` aborts, abort `child` with the same reason.
|
|
39
54
|
*
|
|
@@ -88,6 +103,22 @@ export function abortError(reason?: unknown): Error {
|
|
|
88
103
|
return err;
|
|
89
104
|
}
|
|
90
105
|
|
|
106
|
+
/**
|
|
107
|
+
* Signal for one outbound request: the ambient scope, the caller's signal, or both.
|
|
108
|
+
*
|
|
109
|
+
* Read at request time, not at driver `open`, so an `fx.race` / `fx.all` abort
|
|
110
|
+
* reaches HTTP that is already in flight.
|
|
111
|
+
*
|
|
112
|
+
* @param explicit - Caller-supplied signal, when the API already accepts one
|
|
113
|
+
*/
|
|
114
|
+
export function requestSignal(explicit?: AbortSignal | null): AbortSignal {
|
|
115
|
+
const ambient = currentAbortSignal();
|
|
116
|
+
if (explicit == null || explicit === ambient) return ambient;
|
|
117
|
+
if (ambient.aborted) return ambient;
|
|
118
|
+
if (explicit.aborted) return explicit;
|
|
119
|
+
return AbortSignal.any([ambient, explicit]);
|
|
120
|
+
}
|
|
121
|
+
|
|
91
122
|
/**
|
|
92
123
|
* Sleep that rejects promptly when `signal` aborts.
|
|
93
124
|
*
|
package/src/kernel/app.ts
CHANGED
|
@@ -43,7 +43,7 @@ import { parseAcceptLanguage } from "../elements/channel/locale.ts";
|
|
|
43
43
|
import { runWithLocale } from "../i18n/locale-context.ts";
|
|
44
44
|
import { isFlow, type AnyFlowDef } from "./flow.ts";
|
|
45
45
|
import { failureFromUnknown, runCompensationPhase } from "./compensate.ts";
|
|
46
|
-
import { withAbortSignal } from "./abort-scope.ts";
|
|
46
|
+
import { runDetached, withAbortSignal } from "./abort-scope.ts";
|
|
47
47
|
import { withCdcMutationId } from "../elements/store/sql-session.ts";
|
|
48
48
|
import { MUTATION_ID_HEADER } from "./mutation-id.ts";
|
|
49
49
|
import { fxRetry } from "./concurrency.ts";
|
|
@@ -83,6 +83,7 @@ import {
|
|
|
83
83
|
} from "./hooks.ts";
|
|
84
84
|
import {
|
|
85
85
|
createJournal,
|
|
86
|
+
createJournalSlot,
|
|
86
87
|
createMemoryJournalStore,
|
|
87
88
|
hasJournalLease,
|
|
88
89
|
isJournalLeaseBusy,
|
|
@@ -91,6 +92,7 @@ import {
|
|
|
91
92
|
type JournalSession,
|
|
92
93
|
type JournalStore,
|
|
93
94
|
} from "./journal.ts";
|
|
95
|
+
import type { IdempotencyAttempt } from "./idempotency.ts";
|
|
94
96
|
import { releaseInstanceLeases } from "./graceful-shutdown.ts";
|
|
95
97
|
import { mintInstanceId } from "./instance-id.ts";
|
|
96
98
|
import type { JournalRuntime } from "./boot-bind/journal.ts";
|
|
@@ -181,6 +183,11 @@ function loadFx(): {
|
|
|
181
183
|
return lazyRequire(import.meta.dir, ["fx", "runtime"].join("-"));
|
|
182
184
|
}
|
|
183
185
|
|
|
186
|
+
/** Idempotency policy — off the edge graph until a request actually claims. */
|
|
187
|
+
function loadIdempotency(): typeof import("./idempotency.ts") {
|
|
188
|
+
return lazyRequire(import.meta.dir, ["idem", "potency"].join(""));
|
|
189
|
+
}
|
|
190
|
+
|
|
184
191
|
/** App-shell tenancy wiring — loaded only when `gate.auth.tenant` is on. */
|
|
185
192
|
function loadAppTenant(): { w: (kind: number, ...args: unknown[]) => unknown } {
|
|
186
193
|
return lazyRequire(import.meta.dir, ["app", "tenant"].join("-"));
|
|
@@ -1837,23 +1844,61 @@ export function oke(options: OkeOptions): OkeApp {
|
|
|
1837
1844
|
);
|
|
1838
1845
|
|
|
1839
1846
|
capability = booted.capabilities.get(flowDef.name);
|
|
1840
|
-
|
|
1841
|
-
if (flowDef.durable) {
|
|
1842
|
-
const { store, instanceId, leaseMs } = activeJournal();
|
|
1843
|
-
const journal = createJournal({
|
|
1844
|
-
store,
|
|
1845
|
-
now,
|
|
1846
|
-
id: () => runId,
|
|
1847
|
-
// Hold the run lease for the request's lifetime — a crash mid-run
|
|
1848
|
-
// leaves an expired lease another instance can reclaim and resume.
|
|
1849
|
-
...(hasJournalLease(store) ? { lease: { instanceId, leaseMs } } : {}),
|
|
1850
|
-
});
|
|
1851
|
-
journalSession = await journal.start(flowDef.name, input);
|
|
1852
|
-
inflightRuns.add(journalSession.runId);
|
|
1853
|
-
}
|
|
1854
1847
|
}
|
|
1855
1848
|
|
|
1856
1849
|
const effects = flowDef.effects ?? capability?.declared;
|
|
1850
|
+
const requestForIdem = extras?.request;
|
|
1851
|
+
const rawIdemHeader = requestForIdem?.headers.get("idempotency-key") ?? null;
|
|
1852
|
+
const idemRequired =
|
|
1853
|
+
flowDef.idempotency === "required" ||
|
|
1854
|
+
(typeof flowDef.idempotency === "object" && flowDef.idempotency.required === true) ||
|
|
1855
|
+
flowDef.resolvedIdempotency?.mode === "required";
|
|
1856
|
+
let idemAttempt: IdempotencyAttempt | undefined;
|
|
1857
|
+
if (requestForIdem && (rawIdemHeader !== null || idemRequired)) {
|
|
1858
|
+
const method = trigger.kind === "http" ? trigger.method : requestForIdem.method;
|
|
1859
|
+
const live =
|
|
1860
|
+
flowDef.live !== undefined || (trigger.kind === "http" && trigger.liveSignal !== undefined);
|
|
1861
|
+
idemAttempt = loadIdempotency().prepareIdempotencyAttempt({
|
|
1862
|
+
hasRequest: true,
|
|
1863
|
+
method,
|
|
1864
|
+
stream: flowDef.stream === true,
|
|
1865
|
+
live,
|
|
1866
|
+
effects,
|
|
1867
|
+
usesRaw: flowDef.usesRaw === true,
|
|
1868
|
+
option: flowDef.idempotency,
|
|
1869
|
+
resolved: flowDef.resolvedIdempotency,
|
|
1870
|
+
header: rawIdemHeader,
|
|
1871
|
+
});
|
|
1872
|
+
}
|
|
1873
|
+
const claiming = idemAttempt?.claiming === true;
|
|
1874
|
+
|
|
1875
|
+
const openJournal = (): ReturnType<typeof createJournal> => {
|
|
1876
|
+
const { store, instanceId, leaseMs } = activeJournal();
|
|
1877
|
+
return createJournal({
|
|
1878
|
+
store,
|
|
1879
|
+
now,
|
|
1880
|
+
id: () => runId,
|
|
1881
|
+
// Hold the run lease for the request's lifetime — a crash mid-run
|
|
1882
|
+
// leaves an expired lease another instance can reclaim and resume.
|
|
1883
|
+
...(hasJournalLease(store) ? { lease: { instanceId, leaseMs } } : {}),
|
|
1884
|
+
});
|
|
1885
|
+
};
|
|
1886
|
+
|
|
1887
|
+
// A claim starts the journal only after it wins, so a replay or a 409
|
|
1888
|
+
// does not open a run. Auto with no header keeps the start here.
|
|
1889
|
+
if (booted && flowDef.durable && !claiming) {
|
|
1890
|
+
journalSession = await openJournal().start(flowDef.name, input);
|
|
1891
|
+
inflightRuns.add(journalSession.runId);
|
|
1892
|
+
}
|
|
1893
|
+
|
|
1894
|
+
let journalSlot: { session?: JournalSession } | undefined;
|
|
1895
|
+
let journalFacade: JournalSession | undefined;
|
|
1896
|
+
if (booted && flowDef.durable && claiming) {
|
|
1897
|
+
const created = createJournalSlot();
|
|
1898
|
+
journalSlot = created.slot;
|
|
1899
|
+
journalFacade = created.facade;
|
|
1900
|
+
}
|
|
1901
|
+
const journalForFx = journalSession ?? journalFacade;
|
|
1857
1902
|
const { fx, ledger } = loadFx().createFxContext({
|
|
1858
1903
|
...options.fx,
|
|
1859
1904
|
flow: flowDef.name,
|
|
@@ -1906,7 +1951,7 @@ export function oke(options: OkeOptions): OkeApp {
|
|
|
1906
1951
|
}
|
|
1907
1952
|
: {}),
|
|
1908
1953
|
runsRuntime: booted?.runs ?? (isRunsRuntimeLike(options.runs) ? options.runs : undefined),
|
|
1909
|
-
...(
|
|
1954
|
+
...(journalForFx ? { journal: journalForFx, durable: true } : {}),
|
|
1910
1955
|
callHandler: async (name, callInput) => {
|
|
1911
1956
|
const target = flowsByName.get(name);
|
|
1912
1957
|
if (!target) {
|
|
@@ -2038,13 +2083,141 @@ export function oke(options: OkeOptions): OkeApp {
|
|
|
2038
2083
|
}
|
|
2039
2084
|
return output;
|
|
2040
2085
|
};
|
|
2086
|
+
const runClaimed = async (validated: unknown): Promise<unknown> => {
|
|
2087
|
+
if (!claiming || idemAttempt === undefined) return invoke(validated);
|
|
2088
|
+
const policy = loadIdempotency();
|
|
2089
|
+
const classified = policy.classifyIdempotencyKey(rawIdemHeader);
|
|
2090
|
+
if (classified === "missing") {
|
|
2091
|
+
return policy.idempotencyErrorResponse("IdempotencyKeyMissing");
|
|
2092
|
+
}
|
|
2093
|
+
if (classified === "invalid") {
|
|
2094
|
+
return policy.idempotencyErrorResponse("IdempotencyKeyInvalid");
|
|
2095
|
+
}
|
|
2096
|
+
const idemStore = activeJournal().store.idempotency;
|
|
2097
|
+
if (!idemStore) return invoke(validated);
|
|
2098
|
+
const scope = {
|
|
2099
|
+
tenant: fx.tenant.id ?? "",
|
|
2100
|
+
principal: policy.idempotencyPrincipal(fx.auth),
|
|
2101
|
+
flow: flowDef.name,
|
|
2102
|
+
key: classified,
|
|
2103
|
+
};
|
|
2104
|
+
const claimToken = crypto.randomUUID();
|
|
2105
|
+
const leaseMs = activeJournal().leaseMs;
|
|
2106
|
+
const claimed = await idemStore.claim({
|
|
2107
|
+
scope,
|
|
2108
|
+
fingerprint: policy.idempotencyFingerprint(flowDef.name, validated),
|
|
2109
|
+
claimToken,
|
|
2110
|
+
now: now(),
|
|
2111
|
+
leaseMs,
|
|
2112
|
+
ttlMs: idemAttempt.ttlMs,
|
|
2113
|
+
});
|
|
2114
|
+
if (claimed.kind === "mismatch") {
|
|
2115
|
+
return policy.idempotencyErrorResponse("IdempotencyKeyReused");
|
|
2116
|
+
}
|
|
2117
|
+
if (claimed.kind === "in_progress") {
|
|
2118
|
+
return policy.idempotencyErrorResponse(
|
|
2119
|
+
"IdempotencyInProgress",
|
|
2120
|
+
claimed.retryAfterSeconds,
|
|
2121
|
+
);
|
|
2122
|
+
}
|
|
2123
|
+
if (claimed.kind === "replay") return policy.replayResponse(claimed.row);
|
|
2124
|
+
|
|
2125
|
+
if (booted && flowDef.durable) {
|
|
2126
|
+
const journal = openJournal();
|
|
2127
|
+
const resumeId = claimed.kind === "reclaimed" ? claimed.row.runId : undefined;
|
|
2128
|
+
try {
|
|
2129
|
+
const session = resumeId
|
|
2130
|
+
? await journal.resume(resumeId)
|
|
2131
|
+
: await journal.start(flowDef.name, validated);
|
|
2132
|
+
if (journalSlot) journalSlot.session = session;
|
|
2133
|
+
journalSession = session;
|
|
2134
|
+
inflightRuns.add(session.runId);
|
|
2135
|
+
const tenantId = principals?.tenant.id ?? fx.tenant.id;
|
|
2136
|
+
if (tenantId) await session.stampTenant(tenantId);
|
|
2137
|
+
await idemStore.attachRun(scope, claimToken, session.runId);
|
|
2138
|
+
} catch (err) {
|
|
2139
|
+
if (isJournalLeaseBusy(err)) {
|
|
2140
|
+
await idemStore.forfeit(scope, claimToken);
|
|
2141
|
+
return policy.idempotencyErrorResponse("IdempotencyInProgress", 1);
|
|
2142
|
+
}
|
|
2143
|
+
await idemStore.remove(scope, claimToken);
|
|
2144
|
+
throw err;
|
|
2145
|
+
}
|
|
2146
|
+
}
|
|
2147
|
+
|
|
2148
|
+
const timer = setInterval(
|
|
2149
|
+
() => {
|
|
2150
|
+
void idemStore.renew(scope, claimToken, now() + leaseMs);
|
|
2151
|
+
},
|
|
2152
|
+
Math.max(5, Math.floor(leaseMs / 3)),
|
|
2153
|
+
);
|
|
2154
|
+
if (typeof timer === "object" && timer !== null && "unref" in timer) timer.unref();
|
|
2155
|
+
|
|
2156
|
+
const storeEncoded = async (encoded: {
|
|
2157
|
+
readonly status: number;
|
|
2158
|
+
readonly headers: { get(name: string): string | null };
|
|
2159
|
+
text(): Promise<string>;
|
|
2160
|
+
}): Promise<void> => {
|
|
2161
|
+
const stored = await policy.storedFromResponse(encoded);
|
|
2162
|
+
if (!stored) {
|
|
2163
|
+
await idemStore.remove(scope, claimToken);
|
|
2164
|
+
return;
|
|
2165
|
+
}
|
|
2166
|
+
await idemStore.complete(scope, claimToken, stored, journalSession?.runId);
|
|
2167
|
+
};
|
|
2168
|
+
try {
|
|
2169
|
+
const output = await runDetached(() => invoke(validated));
|
|
2170
|
+
if (loadFx().isJsonStreamResult(output)) {
|
|
2171
|
+
await idemStore.remove(scope, claimToken);
|
|
2172
|
+
return output;
|
|
2173
|
+
}
|
|
2174
|
+
if (output instanceof Response) {
|
|
2175
|
+
const contentType = output.headers.get("content-type") ?? "";
|
|
2176
|
+
const buffered =
|
|
2177
|
+
output.status === 204 ||
|
|
2178
|
+
contentType.includes("json") ||
|
|
2179
|
+
contentType.startsWith("text/");
|
|
2180
|
+
if (!buffered || contentType.includes("text/event-stream")) {
|
|
2181
|
+
await idemStore.remove(scope, claimToken);
|
|
2182
|
+
return output;
|
|
2183
|
+
}
|
|
2184
|
+
await storeEncoded(output.clone());
|
|
2185
|
+
return output;
|
|
2186
|
+
}
|
|
2187
|
+
await storeEncoded(
|
|
2188
|
+
await encodeExecuteResult({
|
|
2189
|
+
output: isFlowFailure(output) ? undefined : output,
|
|
2190
|
+
failure: isFlowFailure(output) ? output : undefined,
|
|
2191
|
+
}),
|
|
2192
|
+
);
|
|
2193
|
+
return output;
|
|
2194
|
+
} catch (err) {
|
|
2195
|
+
if (flowDef.durable && isJournalSuspend(err)) {
|
|
2196
|
+
await idemStore.complete(
|
|
2197
|
+
scope,
|
|
2198
|
+
claimToken,
|
|
2199
|
+
{ status: 204, body: "" },
|
|
2200
|
+
journalSession?.runId,
|
|
2201
|
+
);
|
|
2202
|
+
throw err;
|
|
2203
|
+
}
|
|
2204
|
+
if (policy.attemptedMutation(ledger.entries) || idemAttempt.usesRaw) {
|
|
2205
|
+
await storeEncoded(encodeFailure(fail("InternalError", {})));
|
|
2206
|
+
} else {
|
|
2207
|
+
await idemStore.remove(scope, claimToken);
|
|
2208
|
+
}
|
|
2209
|
+
throw err;
|
|
2210
|
+
} finally {
|
|
2211
|
+
clearInterval(timer);
|
|
2212
|
+
}
|
|
2213
|
+
};
|
|
2041
2214
|
if (!alreadyValidated) {
|
|
2042
2215
|
const parsed = await validate(flowDef.in, ctx.input);
|
|
2043
2216
|
if (!parsed.ok) return parsed.failure;
|
|
2044
2217
|
ctx.input = parsed.value;
|
|
2045
|
-
return await
|
|
2218
|
+
return await runClaimed(parsed.value);
|
|
2046
2219
|
}
|
|
2047
|
-
return await
|
|
2220
|
+
return await runClaimed(ctx.input);
|
|
2048
2221
|
} catch (err) {
|
|
2049
2222
|
if (isFlowFailure(err)) return err;
|
|
2050
2223
|
// A durable sleep that has not yet elapsed suspends the run — this
|
|
@@ -58,7 +58,7 @@ describe("automatic tier-1 cache from effects", () => {
|
|
|
58
58
|
stores: [db],
|
|
59
59
|
gate: { policies: [gate.public] },
|
|
60
60
|
}).adopt({ list });
|
|
61
|
-
const t = await createTestApp(app);
|
|
61
|
+
const t = await createTestApp(app, { capability: "open" });
|
|
62
62
|
const notesApi = requireNotesApi(t);
|
|
63
63
|
|
|
64
64
|
const first = await notesApi.list({});
|
|
@@ -90,7 +90,7 @@ describe("automatic tier-1 cache from effects", () => {
|
|
|
90
90
|
stores: [db],
|
|
91
91
|
gate: { policies: [gate.public] },
|
|
92
92
|
}).adopt({ list });
|
|
93
|
-
const t = await createTestApp(app);
|
|
93
|
+
const t = await createTestApp(app, { capability: "open" });
|
|
94
94
|
const trigger = list.triggers[0];
|
|
95
95
|
if (trigger === undefined) throw new Error("notes.list has no trigger");
|
|
96
96
|
|
|
@@ -128,7 +128,7 @@ describe("automatic tier-1 cache from effects", () => {
|
|
|
128
128
|
stores: [db],
|
|
129
129
|
gate: { policies: [gate.public] },
|
|
130
130
|
}).adopt({ list });
|
|
131
|
-
const t = await createTestApp(app);
|
|
131
|
+
const t = await createTestApp(app, { capability: "open" });
|
|
132
132
|
const notesApi = requireNotesApi(t);
|
|
133
133
|
await notesApi.list({});
|
|
134
134
|
await notesApi.list({});
|
|
@@ -158,7 +158,7 @@ describe("automatic tier-1 cache from effects", () => {
|
|
|
158
158
|
stores: [db],
|
|
159
159
|
gate: { policies: [gate.public] },
|
|
160
160
|
}).adopt({ list });
|
|
161
|
-
const t = await createTestApp(app);
|
|
161
|
+
const t = await createTestApp(app, { capability: "open" });
|
|
162
162
|
const notesApi = requireNotesApi(t);
|
|
163
163
|
|
|
164
164
|
await notesApi.list({});
|
|
@@ -209,7 +209,7 @@ describe("automatic tier-1 cache from effects", () => {
|
|
|
209
209
|
stores: [db],
|
|
210
210
|
gate: { policies: [gate.public] },
|
|
211
211
|
}).adopt({ list, create });
|
|
212
|
-
const t = await createTestApp(app);
|
|
212
|
+
const t = await createTestApp(app, { capability: "open" });
|
|
213
213
|
const notesApi = requireNotesApi(t);
|
|
214
214
|
|
|
215
215
|
await notesApi.list({});
|
|
@@ -251,7 +251,7 @@ describe("automatic tier-1 cache from effects", () => {
|
|
|
251
251
|
stores: [db],
|
|
252
252
|
gate: { policies: [gate.public] },
|
|
253
253
|
}).adopt({ list, create });
|
|
254
|
-
const t = await createTestApp(app);
|
|
254
|
+
const t = await createTestApp(app, { capability: "open" });
|
|
255
255
|
const notesApi = requireNotesApi(t);
|
|
256
256
|
|
|
257
257
|
await notesApi.list({});
|