primitive-admin 1.1.0-alpha.82 → 1.1.0-alpha.84
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/assets/skill/skills/primitive-platform/SKILL.md +105 -675
- package/dist/src/commands/collections.js +61 -0
- package/dist/src/commands/collections.js.map +1 -1
- package/dist/src/commands/databases.js +10 -2
- package/dist/src/commands/databases.js.map +1 -1
- package/dist/src/commands/documents.d.ts +38 -0
- package/dist/src/commands/documents.js +535 -62
- package/dist/src/commands/documents.js.map +1 -1
- package/dist/src/commands/functions.js +303 -121
- package/dist/src/commands/functions.js.map +1 -1
- package/dist/src/commands/locks.js +15 -1
- package/dist/src/commands/locks.js.map +1 -1
- package/dist/src/commands/sync.d.ts +50 -0
- package/dist/src/commands/sync.js +560 -312
- package/dist/src/commands/sync.js.map +1 -1
- package/dist/src/lib/api-client.d.ts +103 -2
- package/dist/src/lib/api-client.js +162 -18
- package/dist/src/lib/api-client.js.map +1 -1
- package/dist/src/lib/config-object-descriptor.js +5 -5
- package/dist/src/lib/config-object-descriptor.js.map +1 -1
- package/dist/src/lib/db-codegen/dbTypeIR.js +8 -0
- package/dist/src/lib/db-codegen/dbTypeIR.js.map +1 -1
- package/dist/src/lib/document-ingest-artifact.d.ts +120 -0
- package/dist/src/lib/document-ingest-artifact.js +505 -0
- package/dist/src/lib/document-ingest-artifact.js.map +1 -0
- package/dist/src/lib/document-ingest-input.d.ts +51 -0
- package/dist/src/lib/document-ingest-input.js +132 -0
- package/dist/src/lib/document-ingest-input.js.map +1 -0
- package/dist/src/lib/document-ingest-rows.d.ts +137 -0
- package/dist/src/lib/document-ingest-rows.js +181 -0
- package/dist/src/lib/document-ingest-rows.js.map +1 -0
- package/dist/src/lib/document-ingest.d.ts +101 -0
- package/dist/src/lib/document-ingest.js +384 -0
- package/dist/src/lib/document-ingest.js.map +1 -0
- package/dist/src/lib/function-bundle.d.ts +6 -0
- package/dist/src/lib/function-bundle.js +8 -0
- package/dist/src/lib/function-bundle.js.map +1 -1
- package/dist/src/lib/function-collect.d.ts +1 -1
- package/dist/src/lib/function-collect.js +16 -0
- package/dist/src/lib/function-collect.js.map +1 -1
- package/dist/src/lib/function-db-types.d.ts +5 -1
- package/dist/src/lib/function-db-types.js +32 -3
- package/dist/src/lib/function-db-types.js.map +1 -1
- package/dist/src/lib/function-document-types.d.ts +83 -0
- package/dist/src/lib/function-document-types.js +155 -1
- package/dist/src/lib/function-document-types.js.map +1 -1
- package/dist/src/lib/function-grants-preflight.js +5 -0
- package/dist/src/lib/function-grants-preflight.js.map +1 -1
- package/dist/src/lib/function-log-lines.d.ts +76 -0
- package/dist/src/lib/function-log-lines.js +160 -0
- package/dist/src/lib/function-log-lines.js.map +1 -0
- package/dist/src/lib/function-log-row.d.ts +29 -0
- package/dist/src/lib/function-log-row.js +73 -0
- package/dist/src/lib/function-log-row.js.map +1 -0
- package/dist/src/lib/function-run.d.ts +53 -35
- package/dist/src/lib/function-run.js +55 -44
- package/dist/src/lib/function-run.js.map +1 -1
- package/dist/src/lib/function-schema-codegen.d.ts +6 -9
- package/dist/src/lib/function-schema-codegen.js +22 -35
- package/dist/src/lib/function-schema-codegen.js.map +1 -1
- package/dist/src/lib/function-sync.d.ts +226 -35
- package/dist/src/lib/function-sync.js +731 -99
- package/dist/src/lib/function-sync.js.map +1 -1
- package/dist/src/lib/function-versions.d.ts +122 -0
- package/dist/src/lib/function-versions.js +182 -0
- package/dist/src/lib/function-versions.js.map +1 -0
- package/dist/src/lib/generated-config-surfaces.d.ts +390 -146
- package/dist/src/lib/generated-config-surfaces.js +1207 -401
- package/dist/src/lib/generated-config-surfaces.js.map +1 -1
- package/dist/src/lib/generated-sdk-types.d.ts +1 -1
- package/dist/src/lib/generated-sdk-types.js +1 -1
- package/dist/src/lib/generated-sdk-types.js.map +1 -1
- package/dist/src/lib/log-inspection.d.ts +2 -0
- package/dist/src/lib/log-inspection.js +4 -0
- package/dist/src/lib/log-inspection.js.map +1 -1
- package/dist/src/lib/output.d.ts +15 -0
- package/dist/src/lib/output.js +28 -0
- package/dist/src/lib/output.js.map +1 -1
- package/dist/src/lib/resolve-owner.d.ts +19 -0
- package/dist/src/lib/resolve-owner.js +20 -0
- package/dist/src/lib/resolve-owner.js.map +1 -0
- package/dist/src/lib/skill-installer.d.ts +49 -3
- package/dist/src/lib/skill-installer.js +275 -100
- package/dist/src/lib/skill-installer.js.map +1 -1
- package/dist/src/lib/snapshot-audit-source.d.ts +45 -0
- package/dist/src/lib/snapshot-audit-source.js +58 -0
- package/dist/src/lib/snapshot-audit-source.js.map +1 -0
- package/dist/src/lib/snapshot-audit-store.d.ts +52 -0
- package/dist/src/lib/snapshot-audit-store.js +196 -0
- package/dist/src/lib/snapshot-audit-store.js.map +1 -0
- package/dist/src/lib/snapshot-audit.d.ts +207 -0
- package/dist/src/lib/snapshot-audit.js +431 -0
- package/dist/src/lib/snapshot-audit.js.map +1 -0
- package/dist/src/lib/snapshot-build-rows.d.ts +60 -0
- package/dist/src/lib/snapshot-build-rows.js +87 -0
- package/dist/src/lib/snapshot-build-rows.js.map +1 -0
- package/dist/src/lib/snapshot-build.d.ts +50 -0
- package/dist/src/lib/snapshot-build.js +111 -0
- package/dist/src/lib/snapshot-build.js.map +1 -0
- package/dist/src/lib/swift-codegen/functionGenerator.d.ts +23 -16
- package/dist/src/lib/swift-codegen/functionGenerator.js +34 -33
- package/dist/src/lib/swift-codegen/functionGenerator.js.map +1 -1
- package/dist/src/lib/sync-selectors.d.ts +45 -2
- package/dist/src/lib/sync-selectors.js +66 -5
- package/dist/src/lib/sync-selectors.js.map +1 -1
- package/package.json +2 -2
|
@@ -38,9 +38,16 @@ export const UPDATE_ONLY = ["update"];
|
|
|
38
38
|
* the definition never dispatches through it (#1976 decision of record); the
|
|
39
39
|
* guard asserts the export exists so a renamed handler fails the CLI unit suite
|
|
40
40
|
* instead of leaving a dangling reference (#2644 behavior 2b).
|
|
41
|
+
*
|
|
42
|
+
* `stage` is REQUIRED (#3375): a field handler that does not say when it runs
|
|
43
|
+
* makes the same empty promise the cross-field slot made before #3373 — it
|
|
44
|
+
* names an owner, and criterion 5 has nothing to check. A `preflight` handler
|
|
45
|
+
* is reached from `cli/src/commands/sync.ts#runConfigPushPreflight` before the
|
|
46
|
+
* first mutating call, which `findUnreachedPreflightRules` proves statically;
|
|
47
|
+
* an `apply` handler is everything else.
|
|
41
48
|
*/
|
|
42
|
-
export function handledBy(modulePath, exportName) {
|
|
43
|
-
return { handler: `${modulePath}#${exportName}
|
|
49
|
+
export function handledBy(modulePath, exportName, stage) {
|
|
50
|
+
return { handler: `${modulePath}#${exportName}`, stage };
|
|
44
51
|
}
|
|
45
52
|
/**
|
|
46
53
|
* Name the ONE export that enforces a cross-field rule, as
|
|
@@ -1395,6 +1402,616 @@ export function crossFieldRulesForStage(surface, stage) {
|
|
|
1395
1402
|
}
|
|
1396
1403
|
return (surface.crossFieldRules ?? []).filter((rule) => rule.stage === stage);
|
|
1397
1404
|
}
|
|
1405
|
+
// ── src/config-surface/preflight-reachability.ts ─────────────────────────
|
|
1406
|
+
/**
|
|
1407
|
+
* Is a rule declared `preflight` actually reached before `config push` mutates
|
|
1408
|
+
* anything? (issue #3375, project phase 3, intent §Success criteria 5.)
|
|
1409
|
+
*
|
|
1410
|
+
* #3373 gave a rule a `stage`; #3374 declared three surfaces' rules. Both left
|
|
1411
|
+
* the string a PROMISE. "This runs before the first mutating call" was
|
|
1412
|
+
* satisfied by typing `preflight` beside a rule that is not on the preflight
|
|
1413
|
+
* path at all, because the push preflight was an inline block inside the
|
|
1414
|
+
* command's `.action()` callback with no exported symbol — so "reached from the
|
|
1415
|
+
* preflight" had nothing to resolve against.
|
|
1416
|
+
*
|
|
1417
|
+
* #3375 extracts that block into `cli/src/commands/sync.ts#runConfigPushPreflight`
|
|
1418
|
+
* and this module answers the question statically, from the declared
|
|
1419
|
+
* `<module>#<export>`:
|
|
1420
|
+
*
|
|
1421
|
+
* - `reachableExports` walks the call graph from one entry symbol over module
|
|
1422
|
+
* SOURCE TEXT and returns what it reached — plus what it could NOT resolve,
|
|
1423
|
+
* so silence is never a pass.
|
|
1424
|
+
* - `findUnreachedPreflightRules` reports every `preflight` declaration,
|
|
1425
|
+
* cross-field or single-field, whose handler is missing from that set.
|
|
1426
|
+
* - `findMalformedFieldValidationStages` is the field-level twin of #3373's
|
|
1427
|
+
* `findMalformedRuleDeclarations`: `ConfigField["validation"]` gained a
|
|
1428
|
+
* `stage` here, and it must be well-formed to mean anything.
|
|
1429
|
+
*
|
|
1430
|
+
* Everything is a PURE function over values and over source text the caller
|
|
1431
|
+
* passes in: `cli/tests/unit/config-surface-preflight-staging-3375.test.ts`
|
|
1432
|
+
* does the filesystem walk, so this module stays vendorable (`types.ts`
|
|
1433
|
+
* §Purity), exactly the split `rules.ts` already uses.
|
|
1434
|
+
*
|
|
1435
|
+
* ── Two things this check is NOT ──────────────────────────────────────────
|
|
1436
|
+
*
|
|
1437
|
+
* It is NECESSARY, not sufficient, and there is deliberately no converse. A
|
|
1438
|
+
* handler reached on one surface's leg satisfies a `preflight` declaration on
|
|
1439
|
+
* another (the residual the intent accepts and #3379 records), and asking "is
|
|
1440
|
+
* this `apply` rule reached from the preflight?" would be wrong by
|
|
1441
|
+
* construction: `validateSigningSecretDeclaration` IS reached on the function
|
|
1442
|
+
* leg and IS correctly `apply` on the webhook surface, where no CLI call
|
|
1443
|
+
* exists.
|
|
1444
|
+
*
|
|
1445
|
+
* The walk is identifier-based over masked source, so it over-approximates in
|
|
1446
|
+
* the PASSING direction: a local variable shadowing an imported handler's name
|
|
1447
|
+
* reads as a call. That is the safe direction for a guard whose failure mode
|
|
1448
|
+
* must be "a promise you did not keep", not "a promise you did keep, reported
|
|
1449
|
+
* as broken".
|
|
1450
|
+
*/
|
|
1451
|
+
/** A field's `validation` object may carry these keys and no others (#3375). */
|
|
1452
|
+
export const FIELD_VALIDATION_KEYS = ["handler", "stage"];
|
|
1453
|
+
const PREFLIGHT_IDENTIFIER = /[A-Za-z_$][\w$]*/g;
|
|
1454
|
+
const PREFLIGHT_DECLARATION = /^(?:export\s+)?(?:default\s+)?(?:declare\s+)?(?:abstract\s+)?(?:async\s+)?(function|class|const|let|var|type|interface|enum)\s*\*?\s+([A-Za-z_$][\w$]*)/;
|
|
1455
|
+
/** The `<module>` and `<export>` halves, or null when the reference is malformed. */
|
|
1456
|
+
function preflightSplitSymbol(symbol) {
|
|
1457
|
+
if (typeof symbol !== "string")
|
|
1458
|
+
return null;
|
|
1459
|
+
const [module, name, ...rest] = symbol.split("#");
|
|
1460
|
+
if (!module || !name || rest.length)
|
|
1461
|
+
return null;
|
|
1462
|
+
return { module, name };
|
|
1463
|
+
}
|
|
1464
|
+
/** Index of the `}`/`)`/`]` closing the bracket at `open`, or -1. */
|
|
1465
|
+
function preflightMatchingBracket(masked, open) {
|
|
1466
|
+
const pairs = { "[": "]", "{": "}", "(": ")" };
|
|
1467
|
+
const stack = [];
|
|
1468
|
+
for (let i = open; i < masked.length; i++) {
|
|
1469
|
+
const ch = masked[i];
|
|
1470
|
+
if (pairs[ch]) {
|
|
1471
|
+
stack.push(pairs[ch]);
|
|
1472
|
+
continue;
|
|
1473
|
+
}
|
|
1474
|
+
if (ch === "]" || ch === "}" || ch === ")") {
|
|
1475
|
+
if (!stack.length || stack.pop() !== ch)
|
|
1476
|
+
return -1;
|
|
1477
|
+
if (!stack.length)
|
|
1478
|
+
return i;
|
|
1479
|
+
}
|
|
1480
|
+
}
|
|
1481
|
+
return -1;
|
|
1482
|
+
}
|
|
1483
|
+
/**
|
|
1484
|
+
* A keyword that can only OPEN a top-level declaration, never continue an
|
|
1485
|
+
* expression: seeing one at column zero ends the statement before it.
|
|
1486
|
+
*
|
|
1487
|
+
* `async` and `abstract` are spelled with what must follow them, because both
|
|
1488
|
+
* can legitimately continue an expression across a line break; `function` and
|
|
1489
|
+
* `class` alone cannot start an indented continuation in a formatted file, and
|
|
1490
|
+
* the column-zero test below is what rules out the expression forms.
|
|
1491
|
+
*/
|
|
1492
|
+
const PREFLIGHT_DECLARATION_OPENS = /^(?:export|import|declare|function|class|const|let|var|type|interface|enum|async\s+function|abstract\s+class)\b/;
|
|
1493
|
+
/**
|
|
1494
|
+
* End of a `const`/`let`/`var`/`type` statement that begins at `from`: the
|
|
1495
|
+
* first `;` outside any bracket, or the line break before the next top-level
|
|
1496
|
+
* declaration — whichever comes first.
|
|
1497
|
+
*
|
|
1498
|
+
* The second boundary is not optional. TypeScript's automatic semicolon
|
|
1499
|
+
* insertion makes `export const entry = () => 1` a complete statement, and a
|
|
1500
|
+
* scan that only ever stops at a `;` runs on through every following
|
|
1501
|
+
* declaration to the next one that has one — attributing an unrelated
|
|
1502
|
+
* apply-only function's body to this declaration, which is an
|
|
1503
|
+
* over-approximation in the PASSING direction: the staging guard would report
|
|
1504
|
+
* a handler as reached that the preflight never mentions. A depth-zero line
|
|
1505
|
+
* break followed by a declaration keyword at column zero is where a formatted
|
|
1506
|
+
* file puts the boundary, and an indented continuation line (`? x`, `: y`,
|
|
1507
|
+
* `| A`, `.then(…)`) never matches it.
|
|
1508
|
+
*
|
|
1509
|
+
* Falling back to the end of the module would let one missing semicolon
|
|
1510
|
+
* attribute the whole rest of the file to a single declaration — an
|
|
1511
|
+
* over-approximation so large the guard would pass on anything. The fallback is
|
|
1512
|
+
* the end of the line instead, which is what an unterminated statement looks
|
|
1513
|
+
* like in a formatted file.
|
|
1514
|
+
*/
|
|
1515
|
+
function preflightStatementEnd(masked, from) {
|
|
1516
|
+
let depth = 0;
|
|
1517
|
+
for (let i = from; i < masked.length; i++) {
|
|
1518
|
+
const ch = masked[i];
|
|
1519
|
+
if (ch === "{" || ch === "(" || ch === "[")
|
|
1520
|
+
depth++;
|
|
1521
|
+
else if (ch === "}" || ch === ")" || ch === "]")
|
|
1522
|
+
depth--;
|
|
1523
|
+
else if (ch === ";" && depth <= 0)
|
|
1524
|
+
return i;
|
|
1525
|
+
else if (ch === "\n" && depth <= 0) {
|
|
1526
|
+
const next = preflightNextSignificant(masked, i + 1);
|
|
1527
|
+
if (next === -1)
|
|
1528
|
+
break;
|
|
1529
|
+
// Column zero only: a continuation line is indented, and a comment ahead
|
|
1530
|
+
// of the next declaration is already blanked to whitespace.
|
|
1531
|
+
if (masked[next - 1] !== "\n")
|
|
1532
|
+
continue;
|
|
1533
|
+
if (PREFLIGHT_DECLARATION_OPENS.test(masked.slice(next, next + 40))) {
|
|
1534
|
+
return i;
|
|
1535
|
+
}
|
|
1536
|
+
}
|
|
1537
|
+
}
|
|
1538
|
+
const newline = masked.indexOf("\n", from);
|
|
1539
|
+
return newline === -1 ? masked.length : newline;
|
|
1540
|
+
}
|
|
1541
|
+
/** Index of the next non-whitespace character at or after `from`, or -1. */
|
|
1542
|
+
function preflightNextSignificant(masked, from) {
|
|
1543
|
+
for (let i = from; i < masked.length; i++) {
|
|
1544
|
+
if (!/\s/.test(masked[i]))
|
|
1545
|
+
return i;
|
|
1546
|
+
}
|
|
1547
|
+
return -1;
|
|
1548
|
+
}
|
|
1549
|
+
/** Index of the previous non-whitespace character before `before`, or -1. */
|
|
1550
|
+
function preflightPreviousSignificant(masked, before) {
|
|
1551
|
+
for (let i = before - 1; i >= 0; i--) {
|
|
1552
|
+
if (!/\s/.test(masked[i]))
|
|
1553
|
+
return i;
|
|
1554
|
+
}
|
|
1555
|
+
return -1;
|
|
1556
|
+
}
|
|
1557
|
+
/**
|
|
1558
|
+
* The character ending the token before `before` — `"` when that token is a
|
|
1559
|
+
* string literal.
|
|
1560
|
+
*
|
|
1561
|
+
* The masker blanks a literal's QUOTES along with its contents, so a
|
|
1562
|
+
* literal type is invisible in the masked view and the search above walks
|
|
1563
|
+
* straight past it to whatever preceded it in the annotation. That matters for
|
|
1564
|
+
* exactly one decision: `function typeOfValue(v): Kind | "null" | "unknown" {`
|
|
1565
|
+
* masks to `… Kind | | {`, whose previous significant
|
|
1566
|
+
* character is a `|` — a type continuation — so the body would be read as an
|
|
1567
|
+
* object type, skipped whole, and the NEXT function's body taken as this one's.
|
|
1568
|
+
* The literal table restores the token the mask erased.
|
|
1569
|
+
*/
|
|
1570
|
+
function preflightPreviousTokenEnd(masked, literals, before) {
|
|
1571
|
+
const previous = preflightPreviousSignificant(masked, before);
|
|
1572
|
+
const gapStart = previous + 1;
|
|
1573
|
+
for (const literal of literals) {
|
|
1574
|
+
if (literal.start >= gapStart && literal.end <= before)
|
|
1575
|
+
return '"';
|
|
1576
|
+
}
|
|
1577
|
+
return previous === -1 ? "" : masked[previous];
|
|
1578
|
+
}
|
|
1579
|
+
/**
|
|
1580
|
+
* A character that can only CONTINUE a type annotation, so a `{` right after it
|
|
1581
|
+
* opens an object type rather than a function body.
|
|
1582
|
+
*/
|
|
1583
|
+
const PREFLIGHT_TYPE_CONTINUES = new Set([
|
|
1584
|
+
":",
|
|
1585
|
+
"|",
|
|
1586
|
+
"&",
|
|
1587
|
+
",",
|
|
1588
|
+
"(",
|
|
1589
|
+
"[",
|
|
1590
|
+
"<",
|
|
1591
|
+
"=",
|
|
1592
|
+
]);
|
|
1593
|
+
/**
|
|
1594
|
+
* Index of the `{` that opens a function's BODY, given the offset just past its
|
|
1595
|
+
* name — or -1 when it has none (an overload signature, a `declare`).
|
|
1596
|
+
*
|
|
1597
|
+
* Not simply the next `{`. A parameter destructured into an inline object type
|
|
1598
|
+
* (`input: { configDir: string; … }`) and a return type naming one
|
|
1599
|
+
* (`Promise<{ parsedVars: … }>`) each open a brace first, and taking either
|
|
1600
|
+
* would end the declaration's span at the signature — after which every symbol
|
|
1601
|
+
* the body really calls is invisible, and a staging check whose walk reaches
|
|
1602
|
+
* nothing passes everything.
|
|
1603
|
+
*
|
|
1604
|
+
* So: match the parameter list, then read the return-type annotation off the
|
|
1605
|
+
* text. A `{` inside a generic argument, or one following a character that can
|
|
1606
|
+
* only continue a type, is skipped whole; the first other `{` is the body. A
|
|
1607
|
+
* `;` reached first means there is no body at all. A literal return type
|
|
1608
|
+
* (`: "ok" {`) is read through the lexer's literal table, because the mask
|
|
1609
|
+
* erased its quotes along with its contents.
|
|
1610
|
+
*/
|
|
1611
|
+
function preflightFunctionBody(masked, literals, afterName) {
|
|
1612
|
+
const paren = masked.indexOf("(", afterName);
|
|
1613
|
+
if (paren === -1)
|
|
1614
|
+
return -1;
|
|
1615
|
+
const closeParen = preflightMatchingBracket(masked, paren);
|
|
1616
|
+
if (closeParen === -1)
|
|
1617
|
+
return -1;
|
|
1618
|
+
let angle = 0;
|
|
1619
|
+
for (let i = closeParen + 1; i < masked.length; i++) {
|
|
1620
|
+
const ch = masked[i];
|
|
1621
|
+
if (ch === ";")
|
|
1622
|
+
return -1; // an overload signature: no body follows
|
|
1623
|
+
if (ch === "<") {
|
|
1624
|
+
angle++;
|
|
1625
|
+
continue;
|
|
1626
|
+
}
|
|
1627
|
+
if (ch === ">" && masked[i - 1] !== "=") {
|
|
1628
|
+
angle = Math.max(0, angle - 1);
|
|
1629
|
+
continue;
|
|
1630
|
+
}
|
|
1631
|
+
if (ch !== "{")
|
|
1632
|
+
continue;
|
|
1633
|
+
const character = preflightPreviousTokenEnd(masked, literals, i);
|
|
1634
|
+
if (angle === 0 && !PREFLIGHT_TYPE_CONTINUES.has(character))
|
|
1635
|
+
return i;
|
|
1636
|
+
const close = preflightMatchingBracket(masked, i);
|
|
1637
|
+
if (close === -1)
|
|
1638
|
+
return -1;
|
|
1639
|
+
i = close;
|
|
1640
|
+
}
|
|
1641
|
+
return -1;
|
|
1642
|
+
}
|
|
1643
|
+
/** Every top-level declaration of a masked module, with its span. */
|
|
1644
|
+
function preflightTopLevelDeclarations(masked, literals) {
|
|
1645
|
+
const out = new Map();
|
|
1646
|
+
/** Names whose recorded span is a signature with no body, so far. */
|
|
1647
|
+
const signatureOnly = new Set();
|
|
1648
|
+
let i = 0;
|
|
1649
|
+
let depth = 0;
|
|
1650
|
+
while (i < masked.length) {
|
|
1651
|
+
const ch = masked[i];
|
|
1652
|
+
const boundary = i === 0 || !/[\w$]/.test(masked[i - 1]);
|
|
1653
|
+
if (depth === 0 && /[A-Za-z_$]/.test(ch) && boundary) {
|
|
1654
|
+
const match = PREFLIGHT_DECLARATION.exec(masked.slice(i, i + 200));
|
|
1655
|
+
if (match) {
|
|
1656
|
+
const kind = match[1];
|
|
1657
|
+
const name = match[2];
|
|
1658
|
+
const afterName = i + match[0].length;
|
|
1659
|
+
let end;
|
|
1660
|
+
let hasBody = true;
|
|
1661
|
+
if (kind === "function" ||
|
|
1662
|
+
kind === "class" ||
|
|
1663
|
+
kind === "interface" ||
|
|
1664
|
+
kind === "enum") {
|
|
1665
|
+
const brace = kind === "function"
|
|
1666
|
+
? preflightFunctionBody(masked, literals, afterName)
|
|
1667
|
+
: masked.indexOf("{", afterName);
|
|
1668
|
+
const close = brace === -1 ? -1 : preflightMatchingBracket(masked, brace);
|
|
1669
|
+
hasBody = close !== -1;
|
|
1670
|
+
end = close === -1 ? preflightStatementEnd(masked, afterName) : close;
|
|
1671
|
+
}
|
|
1672
|
+
else {
|
|
1673
|
+
end = preflightStatementEnd(masked, afterName);
|
|
1674
|
+
}
|
|
1675
|
+
// The declaration with a BODY wins, whichever came first: a `declare`
|
|
1676
|
+
// or an overload signature ahead of the real one carries only types,
|
|
1677
|
+
// and letting it hold the name would hide every call the
|
|
1678
|
+
// implementation makes — the walk would report a genuinely reached
|
|
1679
|
+
// handler as unreached. Between two bodies the first still wins.
|
|
1680
|
+
if (!out.has(name) || (signatureOnly.has(name) && hasBody)) {
|
|
1681
|
+
out.set(name, { start: i, end });
|
|
1682
|
+
if (hasBody)
|
|
1683
|
+
signatureOnly.delete(name);
|
|
1684
|
+
else
|
|
1685
|
+
signatureOnly.add(name);
|
|
1686
|
+
}
|
|
1687
|
+
i = end + 1;
|
|
1688
|
+
continue;
|
|
1689
|
+
}
|
|
1690
|
+
}
|
|
1691
|
+
if (ch === "{" || ch === "(" || ch === "[")
|
|
1692
|
+
depth++;
|
|
1693
|
+
else if (ch === "}" || ch === ")" || ch === "]")
|
|
1694
|
+
depth = Math.max(0, depth - 1);
|
|
1695
|
+
i++;
|
|
1696
|
+
}
|
|
1697
|
+
return out;
|
|
1698
|
+
}
|
|
1699
|
+
/**
|
|
1700
|
+
* Read one module's imports, re-exports and declarations.
|
|
1701
|
+
*
|
|
1702
|
+
* The string CONTENTS are blanked in the masked view — that is what stops a
|
|
1703
|
+
* name inside a comment or a message from reading as a call — so a module
|
|
1704
|
+
* specifier is recovered from the lexer's literal table by position rather than
|
|
1705
|
+
* from the masked text.
|
|
1706
|
+
*/
|
|
1707
|
+
function preflightModuleFacts(source) {
|
|
1708
|
+
const { masked, strings } = lexRuleModuleSource(source);
|
|
1709
|
+
const literalAfter = (index) => {
|
|
1710
|
+
for (const literal of strings) {
|
|
1711
|
+
if (literal.start >= index)
|
|
1712
|
+
return literal.value;
|
|
1713
|
+
}
|
|
1714
|
+
return null;
|
|
1715
|
+
};
|
|
1716
|
+
const imports = new Map();
|
|
1717
|
+
const namespaces = new Map();
|
|
1718
|
+
const namedReExports = new Map();
|
|
1719
|
+
const starReExports = [];
|
|
1720
|
+
/**
|
|
1721
|
+
* `{ a, b as c, type D }` → the value bindings it introduces, as
|
|
1722
|
+
* (name on THIS side, name on the OTHER module's side) pairs. The same
|
|
1723
|
+
* shape reads an import clause and a re-export clause, which are mirrors.
|
|
1724
|
+
*/
|
|
1725
|
+
const readClause = (clause, onBinding) => {
|
|
1726
|
+
for (const piece of clause.split(",")) {
|
|
1727
|
+
const parts = piece.trim().split(/\s+/).filter(Boolean);
|
|
1728
|
+
if (!parts.length || parts[0] === "type")
|
|
1729
|
+
continue; // `{ type X }`
|
|
1730
|
+
const there = parts[0];
|
|
1731
|
+
const here = parts.length >= 3 && parts[1] === "as" ? parts[2] : there;
|
|
1732
|
+
onBinding(here, there);
|
|
1733
|
+
}
|
|
1734
|
+
};
|
|
1735
|
+
// Anchored at the start of a line, and stopping at a `;`: a static import is
|
|
1736
|
+
// always top-level, and an unanchored `\bimport\b` would let a dynamic
|
|
1737
|
+
// `await import(…)` deep inside a function run on to some unrelated `from`
|
|
1738
|
+
// and invent a binding out of whichever string literal came next.
|
|
1739
|
+
for (const match of masked.matchAll(/^import\b([^;]*?)\bfrom\b/gm)) {
|
|
1740
|
+
const clause = match[1];
|
|
1741
|
+
if (/^\s*type\b/.test(clause))
|
|
1742
|
+
continue; // `import type { … } from`
|
|
1743
|
+
const specifier = literalAfter((match.index ?? 0) + match[0].length);
|
|
1744
|
+
if (!specifier)
|
|
1745
|
+
continue;
|
|
1746
|
+
const namespace = /\*\s+as\s+([A-Za-z_$][\w$]*)/.exec(clause);
|
|
1747
|
+
if (namespace)
|
|
1748
|
+
namespaces.set(namespace[1], specifier);
|
|
1749
|
+
const named = /\{([^}]*)\}/.exec(clause);
|
|
1750
|
+
if (named) {
|
|
1751
|
+
readClause(named[1], (here, there) => {
|
|
1752
|
+
imports.set(here, { specifier, imported: there });
|
|
1753
|
+
});
|
|
1754
|
+
}
|
|
1755
|
+
}
|
|
1756
|
+
for (const match of masked.matchAll(/^export\s+(type\s+)?\{([^}]*)\}\s*from\b/gm)) {
|
|
1757
|
+
if (match[1])
|
|
1758
|
+
continue; // `export type { … } from` re-exports no value
|
|
1759
|
+
const specifier = literalAfter((match.index ?? 0) + match[0].length);
|
|
1760
|
+
if (!specifier)
|
|
1761
|
+
continue;
|
|
1762
|
+
readClause(match[2], (here, there) => {
|
|
1763
|
+
namedReExports.set(here, { specifier, local: there });
|
|
1764
|
+
});
|
|
1765
|
+
}
|
|
1766
|
+
for (const match of masked.matchAll(/^export\s*\*\s*from\b/gm)) {
|
|
1767
|
+
const specifier = literalAfter((match.index ?? 0) + match[0].length);
|
|
1768
|
+
if (specifier)
|
|
1769
|
+
starReExports.push(specifier);
|
|
1770
|
+
}
|
|
1771
|
+
return {
|
|
1772
|
+
masked,
|
|
1773
|
+
declarations: preflightTopLevelDeclarations(masked, strings),
|
|
1774
|
+
imports,
|
|
1775
|
+
namespaces,
|
|
1776
|
+
namedReExports,
|
|
1777
|
+
starReExports,
|
|
1778
|
+
};
|
|
1779
|
+
}
|
|
1780
|
+
/**
|
|
1781
|
+
* Walk the call graph from `entry` and return every symbol it reaches.
|
|
1782
|
+
*
|
|
1783
|
+
* An identifier inside a reached declaration's span is followed when it names
|
|
1784
|
+
* another top-level declaration of the same module, or a value import — which
|
|
1785
|
+
* resolves through the graph to whichever candidate module DECLARES the name,
|
|
1786
|
+
* following `export { x } from` and `export * from` on the way. A symbol is
|
|
1787
|
+
* recorded under the module that declares it, never under a barrel that merely
|
|
1788
|
+
* re-exports it: a declaration that names the barrel is reported unreached,
|
|
1789
|
+
* because the point of the string is to name the one implementation.
|
|
1790
|
+
*/
|
|
1791
|
+
export function reachableExports(entry, graph) {
|
|
1792
|
+
const reached = new Set();
|
|
1793
|
+
const unresolved = new Set();
|
|
1794
|
+
const facts = new Map();
|
|
1795
|
+
const queued = new Set();
|
|
1796
|
+
const queue = [];
|
|
1797
|
+
const factsFor = (module) => {
|
|
1798
|
+
const source = graph.modules.get(module);
|
|
1799
|
+
if (source === undefined)
|
|
1800
|
+
return null;
|
|
1801
|
+
let cached = facts.get(module);
|
|
1802
|
+
if (!cached) {
|
|
1803
|
+
cached = preflightModuleFacts(source);
|
|
1804
|
+
facts.set(module, cached);
|
|
1805
|
+
}
|
|
1806
|
+
return cached;
|
|
1807
|
+
};
|
|
1808
|
+
/** Candidate modules for a specifier, reporting a dangling relative one. */
|
|
1809
|
+
const candidates = (from, specifier) => {
|
|
1810
|
+
const resolved = graph.resolve(from, specifier);
|
|
1811
|
+
if (resolved === null)
|
|
1812
|
+
return []; // a package: not followed, not reported
|
|
1813
|
+
if (resolved.length === 0) {
|
|
1814
|
+
unresolved.add(`${from}: "${specifier}" resolves to no module in the graph`);
|
|
1815
|
+
}
|
|
1816
|
+
return resolved;
|
|
1817
|
+
};
|
|
1818
|
+
/**
|
|
1819
|
+
* Where `name` is really DECLARED, starting from `module` — following named
|
|
1820
|
+
* and star re-exports. `[]` when nothing in reach declares it.
|
|
1821
|
+
*/
|
|
1822
|
+
const declaringSymbols = (module, name, seen) => {
|
|
1823
|
+
const key = `${module}#${name}`;
|
|
1824
|
+
if (seen.has(key))
|
|
1825
|
+
return [];
|
|
1826
|
+
seen.add(key);
|
|
1827
|
+
const moduleFacts = factsFor(module);
|
|
1828
|
+
if (!moduleFacts)
|
|
1829
|
+
return [];
|
|
1830
|
+
if (moduleFacts.declarations.has(name))
|
|
1831
|
+
return [key];
|
|
1832
|
+
const out = [];
|
|
1833
|
+
const named = moduleFacts.namedReExports.get(name);
|
|
1834
|
+
if (named) {
|
|
1835
|
+
for (const candidate of candidates(module, named.specifier)) {
|
|
1836
|
+
out.push(...declaringSymbols(candidate, named.local, seen));
|
|
1837
|
+
}
|
|
1838
|
+
}
|
|
1839
|
+
for (const specifier of moduleFacts.starReExports) {
|
|
1840
|
+
for (const candidate of candidates(module, specifier)) {
|
|
1841
|
+
out.push(...declaringSymbols(candidate, name, seen));
|
|
1842
|
+
}
|
|
1843
|
+
}
|
|
1844
|
+
return out;
|
|
1845
|
+
};
|
|
1846
|
+
const enqueue = (symbol) => {
|
|
1847
|
+
if (queued.has(symbol))
|
|
1848
|
+
return;
|
|
1849
|
+
queued.add(symbol);
|
|
1850
|
+
queue.push(symbol);
|
|
1851
|
+
};
|
|
1852
|
+
/** Follow an imported binding to the symbol(s) it really names. */
|
|
1853
|
+
const followImport = (module, specifier, imported) => {
|
|
1854
|
+
for (const candidate of candidates(module, specifier)) {
|
|
1855
|
+
for (const symbol of declaringSymbols(candidate, imported, new Set())) {
|
|
1856
|
+
enqueue(symbol);
|
|
1857
|
+
}
|
|
1858
|
+
}
|
|
1859
|
+
};
|
|
1860
|
+
const parsedEntry = preflightSplitSymbol(entry);
|
|
1861
|
+
if (!parsedEntry) {
|
|
1862
|
+
return {
|
|
1863
|
+
reached,
|
|
1864
|
+
unresolved: [
|
|
1865
|
+
`"${String(entry)}" is not a "<module>#<export>" reference`,
|
|
1866
|
+
],
|
|
1867
|
+
};
|
|
1868
|
+
}
|
|
1869
|
+
const entrySymbols = declaringSymbols(parsedEntry.module, parsedEntry.name, new Set());
|
|
1870
|
+
if (entrySymbols.length === 0) {
|
|
1871
|
+
unresolved.add(`${entry}: no module in the graph declares or re-exports it — the ` +
|
|
1872
|
+
"entry point was renamed, un-exported, or never added to the graph");
|
|
1873
|
+
}
|
|
1874
|
+
for (const symbol of entrySymbols)
|
|
1875
|
+
enqueue(symbol);
|
|
1876
|
+
while (queue.length) {
|
|
1877
|
+
const symbol = queue.shift();
|
|
1878
|
+
const parsed = preflightSplitSymbol(symbol);
|
|
1879
|
+
const moduleFacts = factsFor(parsed.module);
|
|
1880
|
+
if (!moduleFacts)
|
|
1881
|
+
continue;
|
|
1882
|
+
const span = moduleFacts.declarations.get(parsed.name);
|
|
1883
|
+
if (!span)
|
|
1884
|
+
continue;
|
|
1885
|
+
reached.add(symbol);
|
|
1886
|
+
const body = moduleFacts.masked.slice(span.start, span.end);
|
|
1887
|
+
// `ns.member` — the namespace binding's own name says nothing, so the
|
|
1888
|
+
// member is read off the text rather than off the identifier stream.
|
|
1889
|
+
for (const [local, specifier] of moduleFacts.namespaces) {
|
|
1890
|
+
const member = new RegExp(`\\b${local}\\s*\\.\\s*([A-Za-z_$][\\w$]*)`, "g");
|
|
1891
|
+
for (const hit of body.matchAll(member)) {
|
|
1892
|
+
followImport(parsed.module, specifier, hit[1]);
|
|
1893
|
+
}
|
|
1894
|
+
}
|
|
1895
|
+
for (const hit of body.matchAll(PREFLIGHT_IDENTIFIER)) {
|
|
1896
|
+
const name = hit[0];
|
|
1897
|
+
if (name === parsed.name)
|
|
1898
|
+
continue;
|
|
1899
|
+
// A property access is not a reference to a top-level name; `ns.member`
|
|
1900
|
+
// above is the one case where it is, and it is handled there. Walked
|
|
1901
|
+
// backwards rather than by slicing, so a long declaration costs the
|
|
1902
|
+
// walk linear time and not quadratic.
|
|
1903
|
+
let back = (hit.index ?? 0) - 1;
|
|
1904
|
+
while (back >= 0 && /\s/.test(body[back]))
|
|
1905
|
+
back--;
|
|
1906
|
+
if (back >= 0 && body[back] === ".")
|
|
1907
|
+
continue;
|
|
1908
|
+
if (moduleFacts.declarations.has(name)) {
|
|
1909
|
+
enqueue(`${parsed.module}#${name}`);
|
|
1910
|
+
continue;
|
|
1911
|
+
}
|
|
1912
|
+
const imported = moduleFacts.imports.get(name);
|
|
1913
|
+
if (imported) {
|
|
1914
|
+
followImport(parsed.module, imported.specifier, imported.imported);
|
|
1915
|
+
}
|
|
1916
|
+
}
|
|
1917
|
+
}
|
|
1918
|
+
return { reached, unresolved: [...unresolved].sort() };
|
|
1919
|
+
}
|
|
1920
|
+
/** The handler's `<module>` half, or "" when the reference is malformed. */
|
|
1921
|
+
function preflightHandlerModule(handler) {
|
|
1922
|
+
const parsed = preflightSplitSymbol(handler);
|
|
1923
|
+
return parsed ? parsed.module : "";
|
|
1924
|
+
}
|
|
1925
|
+
/**
|
|
1926
|
+
* Every fault in one surface's FIELD validation declarations, each naming the
|
|
1927
|
+
* surface and the field. `[]` means every field is well-formed.
|
|
1928
|
+
*
|
|
1929
|
+
* Runs over every surface in the registry, migrated or not: field declarations
|
|
1930
|
+
* exist independently of the cross-field slot, so the
|
|
1931
|
+
* `CROSS_FIELD_RULES_UNMIGRATED` ratchet does not — and must not — stand
|
|
1932
|
+
* between a field-level `preflight` declaration and this check (DSO-3375-001).
|
|
1933
|
+
*/
|
|
1934
|
+
export function findMalformedFieldValidationStages(surface) {
|
|
1935
|
+
const out = [];
|
|
1936
|
+
for (const table of surface.tables ?? []) {
|
|
1937
|
+
for (const field of table.fields ?? []) {
|
|
1938
|
+
const validation = field.validation;
|
|
1939
|
+
if (validation === "passthrough")
|
|
1940
|
+
continue;
|
|
1941
|
+
const where = `${surface.label}.${field.field}`;
|
|
1942
|
+
if (!validation || typeof validation !== "object") {
|
|
1943
|
+
out.push(`${where}: \`validation\` must be "passthrough" or ` +
|
|
1944
|
+
"`{ handler, stage }`");
|
|
1945
|
+
continue;
|
|
1946
|
+
}
|
|
1947
|
+
const keys = Object.keys(validation).sort();
|
|
1948
|
+
const expected = [...FIELD_VALIDATION_KEYS].sort();
|
|
1949
|
+
if (keys.join(",") !== expected.join(",")) {
|
|
1950
|
+
out.push(`${where}: \`validation\` declares ${keys.join(", ") || "no keys"} — ` +
|
|
1951
|
+
`exactly ${expected.join(" and ")}, and nothing else: the slot ` +
|
|
1952
|
+
"classifies, it does not carry behavior knobs");
|
|
1953
|
+
}
|
|
1954
|
+
const handler = validation.handler;
|
|
1955
|
+
const modulePath = preflightHandlerModule(handler);
|
|
1956
|
+
if (!modulePath) {
|
|
1957
|
+
out.push(`${where}: handler ${JSON.stringify(handler)} is not a ` +
|
|
1958
|
+
'"<module>#<export>" reference');
|
|
1959
|
+
}
|
|
1960
|
+
const stage = validation.stage;
|
|
1961
|
+
if (!CROSS_FIELD_RULE_STAGES.includes(stage)) {
|
|
1962
|
+
out.push(`${where}: stage ${JSON.stringify(stage)} is not one of ` +
|
|
1963
|
+
CROSS_FIELD_RULE_STAGES.join(", ") +
|
|
1964
|
+
" — a field handler says when it runs");
|
|
1965
|
+
}
|
|
1966
|
+
else if (stage === "preflight" &&
|
|
1967
|
+
modulePath &&
|
|
1968
|
+
!modulePath.startsWith(PREFLIGHT_HANDLER_MODULE_PREFIX)) {
|
|
1969
|
+
out.push(`${where}: a \`preflight\` handler is vendored into the CLI, so its ` +
|
|
1970
|
+
`module lives under ${PREFLIGHT_HANDLER_MODULE_PREFIX} — ` +
|
|
1971
|
+
`"${modulePath}" does not`);
|
|
1972
|
+
}
|
|
1973
|
+
}
|
|
1974
|
+
}
|
|
1975
|
+
return out.sort();
|
|
1976
|
+
}
|
|
1977
|
+
/**
|
|
1978
|
+
* Every `preflight` declaration on this surface whose handler the walk did not
|
|
1979
|
+
* reach, each naming the surface, the rule (or field), the handler and the
|
|
1980
|
+
* entry point.
|
|
1981
|
+
*
|
|
1982
|
+
* The FIELD half runs on every surface. The CROSS-FIELD half runs only where a
|
|
1983
|
+
* surface has declared, because `crossFieldRulesForStage` throws for one that
|
|
1984
|
+
* has not — "not declared yet" is never "rule-free" (#3373), and asking it
|
|
1985
|
+
* here would turn an unmigrated surface into a crash rather than a skip.
|
|
1986
|
+
*/
|
|
1987
|
+
export function findUnreachedPreflightRules(surface, reached, entry) {
|
|
1988
|
+
const out = [];
|
|
1989
|
+
const unreached = (subject, handler) => `${surface.label}: ${subject} is declared \`preflight\`, but its handler ` +
|
|
1990
|
+
`${handler} is not reached from ${entry} — a preflight rule runs before ` +
|
|
1991
|
+
"the first mutating call, or it is not preflight";
|
|
1992
|
+
for (const table of surface.tables ?? []) {
|
|
1993
|
+
for (const field of table.fields ?? []) {
|
|
1994
|
+
const validation = field.validation;
|
|
1995
|
+
if (!validation || typeof validation !== "object")
|
|
1996
|
+
continue;
|
|
1997
|
+
const { handler, stage } = validation;
|
|
1998
|
+
if (stage !== "preflight")
|
|
1999
|
+
continue;
|
|
2000
|
+
const reference = String(handler);
|
|
2001
|
+
if (reached.has(reference))
|
|
2002
|
+
continue;
|
|
2003
|
+
out.push(unreached(`field \`${field.field}\``, reference));
|
|
2004
|
+
}
|
|
2005
|
+
}
|
|
2006
|
+
if (hasDeclaredCrossFieldRules(surface)) {
|
|
2007
|
+
for (const rule of crossFieldRulesForStage(surface, "preflight")) {
|
|
2008
|
+
if (reached.has(rule.handler))
|
|
2009
|
+
continue;
|
|
2010
|
+
out.push(unreached(`rule "${rule.id}"`, rule.handler));
|
|
2011
|
+
}
|
|
2012
|
+
}
|
|
2013
|
+
return out.sort();
|
|
2014
|
+
}
|
|
1398
2015
|
/**
|
|
1399
2016
|
* Protocol keys every config UPDATE accepts, whichever object it is.
|
|
1400
2017
|
*
|
|
@@ -2467,144 +3084,170 @@ export function parseFunctionManifest(value) {
|
|
|
2467
3084
|
errors: [],
|
|
2468
3085
|
};
|
|
2469
3086
|
}
|
|
2470
|
-
|
|
2471
|
-
/** The runners, for the doors where a caller states one (#3454, D3454-004). */
|
|
2472
|
-
export const FUNCTION_RUNNERS = ["request", "task"];
|
|
3087
|
+
// ── src/config-surface/function-retired-keys.ts ──────────────────────────
|
|
2473
3088
|
/**
|
|
2474
|
-
* The
|
|
2475
|
-
*
|
|
2476
|
-
*
|
|
2477
|
-
*
|
|
3089
|
+
* The runtime keys a `functions/<key>.toml` no longer decides anything with —
|
|
3090
|
+
* #3482, project `server-functions` phase 4.
|
|
3091
|
+
*
|
|
3092
|
+
* The sponsor settled it on 2026-09-15 (the intent, "Mode vocabulary?",
|
|
3093
|
+
* amended again): the config says NOTHING about how a function runs. A
|
|
3094
|
+
* function is a function; the caller picks the runtime at each call, cron
|
|
3095
|
+
* fires always start a task run, webhook deliveries always run as a request,
|
|
3096
|
+
* and a function that must not run under one runtime tests for it in code with
|
|
3097
|
+
* `assertRuntime`.
|
|
3098
|
+
*
|
|
3099
|
+
* ── Why these keys are accepted, not refused ──────────────────────────────
|
|
3100
|
+
*
|
|
3101
|
+
* Every tree pushed since #3454 that declares a cron trigger carries
|
|
3102
|
+
* `mode = …` ON THE ENTRY, because `cron-trigger-mode-required` demanded it;
|
|
3103
|
+
* plenty carry a top-level `mode` or the older `durable`. Removing the keys
|
|
3104
|
+
* from the accepted set outright would refuse those trees with an unknown-key
|
|
3105
|
+
* error one day after the platform insisted on them, which is the opposite of
|
|
3106
|
+
* principle 5 inside a non-breaking phase.
|
|
3107
|
+
*
|
|
3108
|
+
* So through phase 4 they are ACCEPTED and IGNORED, with one warning per key
|
|
3109
|
+
* naming it and the line to delete; from phase 5 (#3188) they are refused with
|
|
3110
|
+
* the rest of the retirement matrix. The value is not validated — there is
|
|
3111
|
+
* nothing left to validate it against, and refusing a bad value for a key
|
|
3112
|
+
* nobody reads would be a refusal about spelling alone.
|
|
3113
|
+
*
|
|
3114
|
+
* Pure: no models, no tenant context, no `env` (see `types.ts` §Purity). Both
|
|
3115
|
+
* the CLI preflight (vendored by `cli/scripts/gen-config-surfaces.mjs`) and the
|
|
3116
|
+
* admin config-version route read this one scanner, so the warning an author
|
|
3117
|
+
* sees from `config push` is the sentence the API answers with.
|
|
2478
3118
|
*/
|
|
2479
|
-
|
|
2480
|
-
|
|
2481
|
-
|
|
2482
|
-
"
|
|
2483
|
-
|
|
2484
|
-
/** The one refusal constructor (#3373's anchor); `error` keeps its name. */
|
|
2485
|
-
function refuseMode(ruleId, error) {
|
|
2486
|
-
return { ok: false, ruleId, error };
|
|
2487
|
-
}
|
|
2488
|
-
/**
|
|
2489
|
-
* Resolve `mode` and its alias `durable` to one mode.
|
|
2490
|
-
*
|
|
2491
|
-
* Absent both → `any` (#3454): a file that says nothing about its runner takes
|
|
2492
|
-
* either, which is what "a function is a function" means. `mode` must be one
|
|
2493
|
-
* of the three words; `durable` must be a boolean. Both present and
|
|
2494
|
-
* disagreeing is refused naming both keys, so an author who half-migrated a
|
|
2495
|
-
* file hears which line to fix — and `durable` beside `mode = "any"` is one of
|
|
2496
|
-
* those disagreements, because a boolean cannot say "either".
|
|
2497
|
-
*/
|
|
2498
|
-
export function resolveDeclaredMode(input) {
|
|
2499
|
-
const rawMode = input?.mode;
|
|
2500
|
-
const rawDurable = input?.durable;
|
|
2501
|
-
const hasMode = rawMode !== undefined && rawMode !== null;
|
|
2502
|
-
const hasDurable = rawDurable !== undefined && rawDurable !== null;
|
|
2503
|
-
let fromMode = null;
|
|
2504
|
-
if (hasMode) {
|
|
2505
|
-
if (typeof rawMode !== "string" || !FUNCTION_MODES.includes(rawMode)) {
|
|
2506
|
-
return refuseMode("mode-value", `\`mode\` must be "request", "task" or "any" (got ${describe(rawMode)}): ` +
|
|
2507
|
-
'"any" — the default — lets the caller pick, running the function ' +
|
|
2508
|
-
"inside the call with `invoke` or as a background run with `start`; " +
|
|
2509
|
-
'"request" and "task" lock it to one of the two');
|
|
2510
|
-
}
|
|
2511
|
-
fromMode = rawMode;
|
|
2512
|
-
}
|
|
2513
|
-
let fromDurable = null;
|
|
2514
|
-
if (hasDurable) {
|
|
2515
|
-
if (typeof rawDurable !== "boolean") {
|
|
2516
|
-
return refuseMode("durable-value", `\`durable\` must be true or false (got ${describe(rawDurable)}); it is ` +
|
|
2517
|
-
'the accepted alias for `mode = "task"` / `mode = "request"`');
|
|
2518
|
-
}
|
|
2519
|
-
fromDurable = rawDurable ? "task" : "request";
|
|
2520
|
-
}
|
|
2521
|
-
if (fromMode && fromDurable && fromMode !== fromDurable) {
|
|
2522
|
-
// `mode = "any"` beside either boolean lands here too, and deliberately:
|
|
2523
|
-
// the boolean names one runner and `any` names neither, so there is no
|
|
2524
|
-
// value of `durable` the author could have meant. The remedy the message
|
|
2525
|
-
// gives — drop one key — is the same in both cases.
|
|
2526
|
-
return refuseMode("mode-durable-agreement", `\`mode = "${fromMode}"\` and \`durable = ${String(rawDurable)}\` disagree: ` +
|
|
2527
|
-
'`durable` is the accepted alias for `mode = "task"` (true) and ' +
|
|
2528
|
-
'`mode = "request"` (false), and it cannot say `mode = "any"` at all, ' +
|
|
2529
|
-
"so keep one key, or make the two agree");
|
|
2530
|
-
}
|
|
2531
|
-
const mode = fromMode ?? fromDurable ?? "any";
|
|
2532
|
-
return { ok: true, mode, declared: hasMode || hasDurable };
|
|
2533
|
-
}
|
|
3119
|
+
/** The two `[function]` keys that no longer say anything (#3482). */
|
|
3120
|
+
export const RETIRED_FUNCTION_RUNTIME_KEYS = ["mode", "durable"];
|
|
3121
|
+
const RUNTIME_IS_THE_CALLERS = "a function is a function, and the caller picks the runtime at each call — " +
|
|
3122
|
+
"`invoke` runs it inside the request, `start` runs it as a task";
|
|
3123
|
+
const FROM_PHASE_5 = "Remove the line; from phase 5 it is refused.";
|
|
2534
3124
|
/**
|
|
2535
|
-
*
|
|
3125
|
+
* Every retired runtime key a `[function]` table still carries.
|
|
2536
3126
|
*
|
|
2537
|
-
*
|
|
2538
|
-
*
|
|
2539
|
-
* the
|
|
2540
|
-
*
|
|
2541
|
-
*
|
|
2542
|
-
*
|
|
2543
|
-
*
|
|
3127
|
+
* Reads the table the way the author wrote it: the two top-level keys, and a
|
|
3128
|
+
* `mode` on each `[[function.triggers.cron]]` entry. A cron entry's sentence
|
|
3129
|
+
* says the extra thing its author needs to hear — that the schedule no longer
|
|
3130
|
+
* chooses, because every fire starts a task run (#3334's runner branch is
|
|
3131
|
+
* gone).
|
|
3132
|
+
*
|
|
3133
|
+
* Order is the order an author reads their file in: the top-level keys first,
|
|
3134
|
+
* then the cron entries in declaration order.
|
|
2544
3135
|
*/
|
|
2545
|
-
export function
|
|
2546
|
-
const
|
|
2547
|
-
if (typeof
|
|
2548
|
-
return
|
|
3136
|
+
export function ignoredFunctionKeys(functionTable) {
|
|
3137
|
+
const table = functionTable;
|
|
3138
|
+
if (!table || typeof table !== "object" || Array.isArray(table))
|
|
3139
|
+
return [];
|
|
3140
|
+
const found = [];
|
|
3141
|
+
for (const key of RETIRED_FUNCTION_RUNTIME_KEYS) {
|
|
3142
|
+
if (table[key] === undefined)
|
|
3143
|
+
continue;
|
|
3144
|
+
found.push({
|
|
3145
|
+
path: key,
|
|
3146
|
+
message: `\`${key}\` is ignored (#3482): ${RUNTIME_IS_THE_CALLERS}. ${FROM_PHASE_5}`,
|
|
3147
|
+
});
|
|
2549
3148
|
}
|
|
2550
|
-
|
|
2551
|
-
|
|
2552
|
-
|
|
2553
|
-
|
|
2554
|
-
|
|
3149
|
+
const triggers = table.triggers;
|
|
3150
|
+
const rawCron = triggers && typeof triggers === "object" && !Array.isArray(triggers)
|
|
3151
|
+
? triggers.cron
|
|
3152
|
+
: undefined;
|
|
3153
|
+
const entries = Array.isArray(rawCron)
|
|
3154
|
+
? rawCron
|
|
3155
|
+
: rawCron && typeof rawCron === "object"
|
|
3156
|
+
? [rawCron]
|
|
3157
|
+
: [];
|
|
3158
|
+
entries.forEach((entry, index) => {
|
|
3159
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry))
|
|
3160
|
+
return;
|
|
3161
|
+
if (entry.mode === undefined)
|
|
3162
|
+
return;
|
|
3163
|
+
const rawName = entry.name;
|
|
3164
|
+
const name = typeof rawName === "string" && rawName ? rawName : `#${index + 1}`;
|
|
3165
|
+
found.push({
|
|
3166
|
+
path: `triggers.cron.${name}.mode`,
|
|
3167
|
+
message: `\`mode\` on cron trigger '${name}' is ignored (#3482): every cron ` +
|
|
3168
|
+
`fire starts a task run, and ${RUNTIME_IS_THE_CALLERS}. ${FROM_PHASE_5}`,
|
|
3169
|
+
});
|
|
3170
|
+
});
|
|
3171
|
+
return found;
|
|
2555
3172
|
}
|
|
2556
3173
|
/**
|
|
2557
|
-
* The
|
|
2558
|
-
*
|
|
2559
|
-
*
|
|
2560
|
-
*
|
|
2561
|
-
*
|
|
2562
|
-
*
|
|
2563
|
-
*
|
|
3174
|
+
* The database types a `[function]` table's retired trigger block names —
|
|
3175
|
+
* #3483.
|
|
3176
|
+
*
|
|
3177
|
+
* Beside `ignoredFunctionKeys` because it answers the same question about the
|
|
3178
|
+
* same table, for the same reader: `config pull` writes a version's AUTHORED
|
|
3179
|
+
* bytes, and a version pushed before the removal carries the block. Without
|
|
3180
|
+
* this the pulled tree would be refused on its next push — a tree an author
|
|
3181
|
+
* cannot get back into a working state, which is the failure D3482-007 already
|
|
3182
|
+
* had to close for the runtime keys.
|
|
3183
|
+
*
|
|
3184
|
+
* This is NOT a refusal and shares nothing with one. The block is refused on
|
|
3185
|
+
* input by `normalizeTriggerDeclaration`; what a strip needs is the list of
|
|
3186
|
+
* entries it is taking out so it can say so, which is why an entry whose
|
|
3187
|
+
* `type` is unreadable still counts as one entry (the empty string). It is the
|
|
3188
|
+
* block's PRESENCE that goes, not its contents.
|
|
3189
|
+
*
|
|
3190
|
+
* TOML spells the block three ways — an array of tables, a single table, and
|
|
3191
|
+
* an inline array — and a scan that missed one would leave a key behind
|
|
3192
|
+
* (CR3482-002's lesson, applied to a whole table).
|
|
2564
3193
|
*/
|
|
2565
|
-
export function
|
|
2566
|
-
|
|
2567
|
-
|
|
2568
|
-
|
|
3194
|
+
export function retiredDatabaseTriggerTypes(functionTable) {
|
|
3195
|
+
const raw = retiredDatabaseTriggerValue(functionTable);
|
|
3196
|
+
if (raw === undefined || raw === null)
|
|
3197
|
+
return [];
|
|
3198
|
+
const entries = Array.isArray(raw) ? raw : [raw];
|
|
3199
|
+
return entries.map((entry) => {
|
|
3200
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry))
|
|
3201
|
+
return "";
|
|
3202
|
+
const type = entry.type;
|
|
3203
|
+
return typeof type === "string" ? type : String(type ?? "");
|
|
3204
|
+
});
|
|
2569
3205
|
}
|
|
2570
|
-
|
|
2571
|
-
|
|
2572
|
-
|
|
2573
|
-
|
|
2574
|
-
|
|
2575
|
-
|
|
2576
|
-
|
|
2577
|
-
|
|
3206
|
+
/**
|
|
3207
|
+
* Whether the block is THERE at all — the question a strip has to ask, which
|
|
3208
|
+
* the type list above cannot answer (CR3483-001).
|
|
3209
|
+
*
|
|
3210
|
+
* `database = []` names no type, so the list is empty and a strip that decided
|
|
3211
|
+
* by its length left the key in the pulled file. That file then fails its own
|
|
3212
|
+
* next push: `normalizeTriggerDeclaration` refuses the key by PRESENCE, of any
|
|
3213
|
+
* shape, an empty array included — so the two halves disagreed about what the
|
|
3214
|
+
* block is, and the tree an author pulled was a tree they could not push.
|
|
3215
|
+
* Presence is the one rule both sides read now, spelled the way
|
|
3216
|
+
* `normalizeTriggerDeclaration` spells it — `!== undefined` — so a value one
|
|
3217
|
+
* of them would call a block and the other would not cannot exist.
|
|
3218
|
+
*/
|
|
3219
|
+
export function hasRetiredDatabaseTriggerBlock(functionTable) {
|
|
3220
|
+
return retiredDatabaseTriggerValue(functionTable) !== undefined;
|
|
2578
3221
|
}
|
|
2579
3222
|
/**
|
|
2580
|
-
* The
|
|
2581
|
-
*
|
|
2582
|
-
*
|
|
2583
|
-
* answer `resolveDeclaredMode` gives a file that says nothing. Every
|
|
2584
|
-
* authoritative door resolves and passes explicitly; this keeps the two
|
|
2585
|
-
* readings one reading rather than letting an omission mean something a
|
|
2586
|
-
* declaration cannot.
|
|
3223
|
+
* The raw `[function.triggers].database` value, or `undefined` when the block
|
|
3224
|
+
* is absent. One walk down to the key, so presence and contents can never be
|
|
3225
|
+
* read off two different traversals.
|
|
2587
3226
|
*/
|
|
2588
|
-
function
|
|
2589
|
-
|
|
3227
|
+
function retiredDatabaseTriggerValue(functionTable) {
|
|
3228
|
+
const table = functionTable;
|
|
3229
|
+
if (!table || typeof table !== "object" || Array.isArray(table)) {
|
|
3230
|
+
return undefined;
|
|
3231
|
+
}
|
|
3232
|
+
const triggers = table.triggers;
|
|
3233
|
+
if (!triggers || typeof triggers !== "object" || Array.isArray(triggers)) {
|
|
3234
|
+
return undefined;
|
|
3235
|
+
}
|
|
3236
|
+
return triggers.database;
|
|
2590
3237
|
}
|
|
2591
3238
|
/**
|
|
2592
|
-
*
|
|
3239
|
+
* The same scan, rendered for one file — the shape `config push` and
|
|
3240
|
+
* `config diff` print and the admin route answers with.
|
|
2593
3241
|
*
|
|
2594
|
-
*
|
|
2595
|
-
*
|
|
2596
|
-
* half that was missing — an author who meets this at the push door could not
|
|
2597
|
-
* previously tell whether it was a limitation or a design, nor what to write.
|
|
3242
|
+
* `label` is what the reader can act on: `functions/<key>.toml` from the CLI,
|
|
3243
|
+
* and the function's key from the API, which has no file to name.
|
|
2598
3244
|
*/
|
|
2599
|
-
export
|
|
2600
|
-
|
|
2601
|
-
|
|
2602
|
-
"the request function, then hand the work to a task function with " +
|
|
2603
|
-
"ctx.functions.start — or remove [function.triggers.webhook]";
|
|
3245
|
+
export function ignoredFunctionKeyWarnings(label, functionTable) {
|
|
3246
|
+
return ignoredFunctionKeys(functionTable).map((ignored) => `${label}: ${ignored.message}`);
|
|
3247
|
+
}
|
|
2604
3248
|
export const EMPTY_TRIGGER_DECLARATION = {
|
|
2605
3249
|
webhook: null,
|
|
2606
3250
|
cron: [],
|
|
2607
|
-
databases: [],
|
|
2608
3251
|
};
|
|
2609
3252
|
/**
|
|
2610
3253
|
* The most cron entries one function may declare.
|
|
@@ -2622,22 +3265,6 @@ export const MAX_CRON_TRIGGERS_PER_FUNCTION = 10;
|
|
|
2622
3265
|
* collide with (the intent, "Key namespace?").
|
|
2623
3266
|
*/
|
|
2624
3267
|
const CRON_NAME_PATTERN = /^[a-zA-Z0-9][a-zA-Z0-9_-]*$/;
|
|
2625
|
-
/**
|
|
2626
|
-
* The most database types one function may watch (#3184, D-07).
|
|
2627
|
-
*
|
|
2628
|
-
* Per-OBJECT, like the cron cap above: a function watching more than a handful
|
|
2629
|
-
* of types is describing a fan-in the platform cannot bound per write, and
|
|
2630
|
-
* every extra type is another discovery read on somebody's write path.
|
|
2631
|
-
*/
|
|
2632
|
-
export const MAX_DATABASE_TRIGGERS_PER_FUNCTION = 5;
|
|
2633
|
-
/**
|
|
2634
|
-
* A database type key, matching the charset the grant grammar already uses for
|
|
2635
|
-
* the same value (`src/config-surface/function-grants.ts`). One rule for one
|
|
2636
|
-
* concept: `database:<type>/<model>:read` and `[[function.triggers.database]]`
|
|
2637
|
-
* name the same thing.
|
|
2638
|
-
*/
|
|
2639
|
-
const DATABASE_TYPE_PATTERN = /^[A-Za-z0-9_-]+$/;
|
|
2640
|
-
const DATABASE_KEYS = new Set(["type"]);
|
|
2641
3268
|
const WEBHOOK_KEYS = new Set([
|
|
2642
3269
|
"verificationScheme",
|
|
2643
3270
|
"signingSecret",
|
|
@@ -2651,6 +3278,12 @@ const WEBHOOK_KEYS = new Set([
|
|
|
2651
3278
|
const CRON_KEYS = new Set([
|
|
2652
3279
|
"name",
|
|
2653
3280
|
"cron",
|
|
3281
|
+
// #3482 — RETIRED and still accepted. `cron-trigger-mode-required` demanded
|
|
3282
|
+
// this key on every default-mode function since #3454, so every tree pushed
|
|
3283
|
+
// with a cron entry since then carries it; dropping it from the accepted set
|
|
3284
|
+
// would refuse those trees with an unknown-key error one day after the
|
|
3285
|
+
// platform insisted on it (D3482-006). Ignored, warned about at the push
|
|
3286
|
+
// door, and refused from phase 5.
|
|
2654
3287
|
"mode",
|
|
2655
3288
|
"timezone",
|
|
2656
3289
|
"overlapPolicy",
|
|
@@ -2666,17 +3299,12 @@ const VALID_OVERLAP_POLICIES = ["skip", "allow"];
|
|
|
2666
3299
|
export const TRIGGER_RULE_IDS = [
|
|
2667
3300
|
"triggers-table",
|
|
2668
3301
|
"trigger-kind",
|
|
2669
|
-
|
|
2670
|
-
|
|
2671
|
-
"database-trigger-
|
|
2672
|
-
"database-trigger-keys",
|
|
2673
|
-
"database-trigger-type-required",
|
|
2674
|
-
"database-trigger-type-charset",
|
|
2675
|
-
"database-trigger-duplicate",
|
|
3302
|
+
// #3483 — one refusal replaced the eight that shaped the block. There is no
|
|
3303
|
+
// shape left to get right; the block itself is what is refused.
|
|
3304
|
+
"database-trigger-removed",
|
|
2676
3305
|
"webhook-trigger-single",
|
|
2677
3306
|
"webhook-trigger-table",
|
|
2678
3307
|
"webhook-trigger-keys",
|
|
2679
|
-
"webhook-trigger-mode",
|
|
2680
3308
|
"webhook-trigger-scheme-required",
|
|
2681
3309
|
"webhook-trigger-scheme-none",
|
|
2682
3310
|
"webhook-trigger-number",
|
|
@@ -2690,9 +3318,6 @@ export const TRIGGER_RULE_IDS = [
|
|
|
2690
3318
|
"cron-trigger-duplicate",
|
|
2691
3319
|
"cron-trigger-expression-required",
|
|
2692
3320
|
"cron-trigger-overlap-policy",
|
|
2693
|
-
"cron-trigger-mode-value",
|
|
2694
|
-
"cron-trigger-mode-required",
|
|
2695
|
-
"cron-trigger-mode-locked",
|
|
2696
3321
|
];
|
|
2697
3322
|
/**
|
|
2698
3323
|
* A broken trigger rule. `ruleId` names which one, for a reader that wants to
|
|
@@ -2713,9 +3338,7 @@ function refuseTrigger(ruleId, message) {
|
|
|
2713
3338
|
}
|
|
2714
3339
|
/** Is anything at all declared? Used to skip work on the common case. */
|
|
2715
3340
|
export function hasDeclaredTriggers(declaration) {
|
|
2716
|
-
return
|
|
2717
|
-
declaration.cron.length > 0 ||
|
|
2718
|
-
declaration.databases.length > 0);
|
|
3341
|
+
return declaration.webhook !== null || declaration.cron.length > 0;
|
|
2719
3342
|
}
|
|
2720
3343
|
/**
|
|
2721
3344
|
* Normalize the `triggers` table of an already-parsed `[function]` table.
|
|
@@ -2726,7 +3349,7 @@ export function hasDeclaredTriggers(declaration) {
|
|
|
2726
3349
|
* push boundary can normalize a declaration it read from a stored row without
|
|
2727
3350
|
* re-parsing bytes.
|
|
2728
3351
|
*/
|
|
2729
|
-
export function normalizeTriggerDeclaration(functionTable
|
|
3352
|
+
export function normalizeTriggerDeclaration(functionTable) {
|
|
2730
3353
|
const triggers = functionTable?.triggers;
|
|
2731
3354
|
if (triggers === undefined || triggers === null) {
|
|
2732
3355
|
return EMPTY_TRIGGER_DECLARATION;
|
|
@@ -2734,75 +3357,41 @@ export function normalizeTriggerDeclaration(functionTable, options = {}) {
|
|
|
2734
3357
|
if (typeof triggers !== "object" || Array.isArray(triggers)) {
|
|
2735
3358
|
refuseTrigger("triggers-table", "[function.triggers] must be a table declaring `webhook` and/or `cron`");
|
|
2736
3359
|
}
|
|
3360
|
+
// #3483 — checked BEFORE the unknown-kind rule, so an author who declares
|
|
3361
|
+
// the block hears why it went rather than that `database` is not a word the
|
|
3362
|
+
// platform knows. Any shape of the key is the same refusal: it is the
|
|
3363
|
+
// block's PRESENCE that is refused, and an author who wrote
|
|
3364
|
+
// `triggers.database = []` still has a line to delete.
|
|
3365
|
+
if (triggers.database !== undefined) {
|
|
3366
|
+
refuseTrigger("database-trigger-removed", DATABASE_TRIGGER_REMOVED);
|
|
3367
|
+
}
|
|
2737
3368
|
for (const key of Object.keys(triggers)) {
|
|
2738
|
-
if (key !== "webhook" && key !== "cron"
|
|
3369
|
+
if (key !== "webhook" && key !== "cron") {
|
|
2739
3370
|
refuseTrigger("trigger-kind", `unknown trigger kind '${key}' under [function.triggers]: the kinds are ` +
|
|
2740
|
-
"`webhook
|
|
3371
|
+
"`webhook` and `cron`");
|
|
2741
3372
|
}
|
|
2742
3373
|
}
|
|
2743
|
-
const webhook = normalizeWebhook(triggers.webhook
|
|
2744
|
-
const cron = normalizeCron(triggers.cron
|
|
2745
|
-
|
|
2746
|
-
return { webhook, cron, databases };
|
|
3374
|
+
const webhook = normalizeWebhook(triggers.webhook);
|
|
3375
|
+
const cron = normalizeCron(triggers.cron);
|
|
3376
|
+
return { webhook, cron };
|
|
2747
3377
|
}
|
|
2748
3378
|
/**
|
|
2749
|
-
*
|
|
2750
|
-
*
|
|
2751
|
-
*
|
|
2752
|
-
*
|
|
2753
|
-
*
|
|
2754
|
-
*
|
|
3379
|
+
* The sentence an author who declared the block is owed — D3483-001.
|
|
3380
|
+
*
|
|
3381
|
+
* It names the pattern that replaces the trigger rather than only saying the
|
|
3382
|
+
* trigger is gone, because the author's problem is not "how do I spell this"
|
|
3383
|
+
* but "where does this work go now". The answer is: into the handler that
|
|
3384
|
+
* writes the row. Database access moves behind server functions (phase 6
|
|
3385
|
+
* retires the client's direct database paths), so the code that writes a
|
|
3386
|
+
* record is already a function — a trigger fired by that write has no caller
|
|
3387
|
+
* it cannot already see.
|
|
2755
3388
|
*/
|
|
2756
|
-
function
|
|
2757
|
-
|
|
2758
|
-
|
|
2759
|
-
|
|
2760
|
-
|
|
2761
|
-
|
|
2762
|
-
if (modeOf(options) === "task") {
|
|
2763
|
-
refuseTrigger("database-trigger-mode", "database triggers require request mode: set `mode = \"request\"` (or " +
|
|
2764
|
-
"remove `mode = \"task\"`), or remove the [[function.triggers.database]] " +
|
|
2765
|
-
"entries");
|
|
2766
|
-
}
|
|
2767
|
-
if (entries.length > MAX_DATABASE_TRIGGERS_PER_FUNCTION) {
|
|
2768
|
-
refuseTrigger("database-trigger-cap", `a function may watch at most ${MAX_DATABASE_TRIGGERS_PER_FUNCTION} database ` +
|
|
2769
|
-
`types, and this one declares ${entries.length}`);
|
|
2770
|
-
}
|
|
2771
|
-
const seen = new Set();
|
|
2772
|
-
const normalized = [];
|
|
2773
|
-
for (const entry of entries) {
|
|
2774
|
-
if (entry === null || typeof entry !== "object" || Array.isArray(entry)) {
|
|
2775
|
-
refuseTrigger("database-trigger-entry", "each [[function.triggers.database]] entry must be a table");
|
|
2776
|
-
}
|
|
2777
|
-
const table = entry;
|
|
2778
|
-
for (const key of Object.keys(table)) {
|
|
2779
|
-
if (!DATABASE_KEYS.has(key)) {
|
|
2780
|
-
refuseTrigger("database-trigger-keys", `unknown key '${key}' in a [[function.triggers.database]] entry: the ` +
|
|
2781
|
-
`only accepted key is ${[...DATABASE_KEYS].join(", ")}`);
|
|
2782
|
-
}
|
|
2783
|
-
}
|
|
2784
|
-
const type = String(table.type ?? "").trim();
|
|
2785
|
-
if (!type) {
|
|
2786
|
-
refuseTrigger("database-trigger-type-required", "each [[function.triggers.database]] entry needs a `type`: it names the " +
|
|
2787
|
-
"database type whose writes fire the function");
|
|
2788
|
-
}
|
|
2789
|
-
if (!DATABASE_TYPE_PATTERN.test(type)) {
|
|
2790
|
-
refuseTrigger("database-trigger-type-charset", `database trigger type '${type}' must contain only letters, digits, ` +
|
|
2791
|
-
"'-' or '_'");
|
|
2792
|
-
}
|
|
2793
|
-
if (seen.has(type)) {
|
|
2794
|
-
refuseTrigger("database-trigger-duplicate", `duplicate database trigger type '${type}': a function fires once per ` +
|
|
2795
|
-
"write request, so each type is declared once");
|
|
2796
|
-
}
|
|
2797
|
-
seen.add(type);
|
|
2798
|
-
normalized.push({ type });
|
|
2799
|
-
}
|
|
2800
|
-
// Sorted, like the cron entries: two files that differ only in the order
|
|
2801
|
-
// they list their blocks describe the same triggers.
|
|
2802
|
-
normalized.sort((a, b) => a.type.localeCompare(b.type));
|
|
2803
|
-
return normalized;
|
|
2804
|
-
}
|
|
2805
|
-
function normalizeWebhook(raw, options) {
|
|
3389
|
+
const DATABASE_TRIGGER_REMOVED = "[[function.triggers.database]] is no longer supported (#3483): database " +
|
|
3390
|
+
"access moves behind functions, so the code that writes the record is a " +
|
|
3391
|
+
"function — do the follow-up in the handler that writes (write the row, " +
|
|
3392
|
+
"then publish, notify or write again), or hand longer work to a task with " +
|
|
3393
|
+
"ctx.functions.start. Remove the block.";
|
|
3394
|
+
function normalizeWebhook(raw) {
|
|
2806
3395
|
if (raw === undefined || raw === null)
|
|
2807
3396
|
return null;
|
|
2808
3397
|
if (Array.isArray(raw)) {
|
|
@@ -2820,14 +3409,6 @@ function normalizeWebhook(raw, options) {
|
|
|
2820
3409
|
`are ${[...WEBHOOK_KEYS].join(", ")}`);
|
|
2821
3410
|
}
|
|
2822
3411
|
}
|
|
2823
|
-
// The intent forbids this permanently: a webhook fires the function inside
|
|
2824
|
-
// the receiver, and the provider is holding the connection open waiting for
|
|
2825
|
-
// its answer. A task run has no answer to give inside that request — it
|
|
2826
|
-
// returns a run id and finishes later — so the refusal says so and names
|
|
2827
|
-
// the pattern that does the same job (#3334).
|
|
2828
|
-
if (modeOf(options) === "task") {
|
|
2829
|
-
refuseTrigger("webhook-trigger-mode", WEBHOOK_ON_TASK_REFUSAL);
|
|
2830
|
-
}
|
|
2831
3412
|
const scheme = String(table.verificationScheme ?? "").trim();
|
|
2832
3413
|
if (!scheme) {
|
|
2833
3414
|
refuseTrigger("webhook-trigger-scheme-required", "[function.triggers.webhook] verificationScheme is required");
|
|
@@ -2871,18 +3452,12 @@ function normalizeWebhook(raw, options) {
|
|
|
2871
3452
|
}
|
|
2872
3453
|
return declaration;
|
|
2873
3454
|
}
|
|
2874
|
-
function normalizeCron(raw
|
|
3455
|
+
function normalizeCron(raw) {
|
|
2875
3456
|
if (raw === undefined || raw === null)
|
|
2876
3457
|
return [];
|
|
2877
3458
|
const entries = Array.isArray(raw) ? raw : [raw];
|
|
2878
3459
|
if (entries.length === 0)
|
|
2879
3460
|
return [];
|
|
2880
|
-
// No refusal for the function's own mode: a cron entry is legal in EVERY
|
|
2881
|
-
// mode (#3334). The intent allows it ("Sync functions write a run row?":
|
|
2882
|
-
// *HTTP and cron allow both*), and the cron DO starts a durable run for a
|
|
2883
|
-
// task target. What each ENTRY is checked for is which runner ITS fires use
|
|
2884
|
-
// (#3454) — see the three `cron-trigger-mode-*` refusals below.
|
|
2885
|
-
const functionMode = modeOf(options);
|
|
2886
3461
|
if (entries.length > MAX_CRON_TRIGGERS_PER_FUNCTION) {
|
|
2887
3462
|
refuseTrigger("cron-trigger-cap", `a function may declare at most ${MAX_CRON_TRIGGERS_PER_FUNCTION} cron ` +
|
|
2888
3463
|
`trigger entries, and this one declares ${entries.length}`);
|
|
@@ -2918,27 +3493,10 @@ function normalizeCron(raw, options) {
|
|
|
2918
3493
|
if (!cron) {
|
|
2919
3494
|
refuseTrigger("cron-trigger-expression-required", `cron trigger '${name}' needs a \`cron\` expression`);
|
|
2920
3495
|
}
|
|
2921
|
-
//
|
|
3496
|
+
// #3482 — no runner branch here any more. Every cron fire starts a task
|
|
3497
|
+
// run, so an entry's `mode` decides nothing; it is accepted (D3482-006),
|
|
3498
|
+
// reported as ignored by `ignoredFunctionKeys`, and dropped.
|
|
2922
3499
|
const declaration = { name, cron };
|
|
2923
|
-
if (table.mode !== undefined && table.mode !== null) {
|
|
2924
|
-
const entryMode = String(table.mode);
|
|
2925
|
-
if (!FUNCTION_RUNNERS.includes(entryMode)) {
|
|
2926
|
-
refuseTrigger("cron-trigger-mode-value", `cron trigger '${name}': \`mode\` must be "request" or "task" (got ` +
|
|
2927
|
-
`${JSON.stringify(table.mode)}): a fire runs on one runner, so the ` +
|
|
2928
|
-
'entry names one — "any" is a mode a FUNCTION may have, not a runner');
|
|
2929
|
-
}
|
|
2930
|
-
if (functionMode !== "any" && functionMode !== entryMode) {
|
|
2931
|
-
refuseTrigger("cron-trigger-mode-locked", `cron trigger '${name}' says \`mode = "${entryMode}"\` but the ` +
|
|
2932
|
-
`function is locked to ${functionMode}: drop the entry's mode or ` +
|
|
2933
|
-
"change the function's");
|
|
2934
|
-
}
|
|
2935
|
-
declaration.mode = entryMode;
|
|
2936
|
-
}
|
|
2937
|
-
else if (functionMode === "any") {
|
|
2938
|
-
refuseTrigger("cron-trigger-mode-required", `cron trigger '${name}': the function's mode is any, so the entry says ` +
|
|
2939
|
-
'which runner each fire uses: add `mode = "request"` or ' +
|
|
2940
|
-
'`mode = "task"`');
|
|
2941
|
-
}
|
|
2942
3500
|
if (table.timezone !== undefined) {
|
|
2943
3501
|
declaration.timezone = String(table.timezone);
|
|
2944
3502
|
}
|
|
@@ -3006,15 +3564,39 @@ export function parseStoredTriggerDeclaration(stored) {
|
|
|
3006
3564
|
return {
|
|
3007
3565
|
webhook: parsed?.webhook ?? null,
|
|
3008
3566
|
cron: Array.isArray(parsed?.cron) ? parsed.cron : [],
|
|
3009
|
-
//
|
|
3010
|
-
//
|
|
3011
|
-
|
|
3567
|
+
// #3483 — a stored `databases` key is DROPPED, not read. A version
|
|
3568
|
+
// pushed before the refusal existed still carries one in its immutable
|
|
3569
|
+
// JSON, and the row is never rewritten; answering nothing at all is what
|
|
3570
|
+
// makes that version inert at every reader at once.
|
|
3571
|
+
// `storedDatabaseTriggerTypes` below is how a caller that wants to SAY
|
|
3572
|
+
// something about it still can.
|
|
3012
3573
|
};
|
|
3013
3574
|
}
|
|
3014
3575
|
catch {
|
|
3015
3576
|
return EMPTY_TRIGGER_DECLARATION;
|
|
3016
3577
|
}
|
|
3017
3578
|
}
|
|
3579
|
+
/**
|
|
3580
|
+
* The database types a STORED declaration used to watch, for a reader that
|
|
3581
|
+
* wants to name them — #3483.
|
|
3582
|
+
*
|
|
3583
|
+
* Pure, and separate from the parse on purpose: the parse answers what the
|
|
3584
|
+
* version means NOW (nothing), and this answers what it said, which is only
|
|
3585
|
+
* ever used to write one log line. Nothing branches on it.
|
|
3586
|
+
*/
|
|
3587
|
+
export function storedDatabaseTriggerTypes(stored) {
|
|
3588
|
+
if (typeof stored !== "string" || !stored.trim())
|
|
3589
|
+
return [];
|
|
3590
|
+
try {
|
|
3591
|
+
const parsed = JSON.parse(stored);
|
|
3592
|
+
if (!Array.isArray(parsed?.databases))
|
|
3593
|
+
return [];
|
|
3594
|
+
return parsed.databases.map((entry) => String(entry?.type ?? ""));
|
|
3595
|
+
}
|
|
3596
|
+
catch {
|
|
3597
|
+
return [];
|
|
3598
|
+
}
|
|
3599
|
+
}
|
|
3018
3600
|
/** The `CronTrigger.triggerKey` an entry owns. */
|
|
3019
3601
|
export function cronTriggerKeyFor(functionKey, name) {
|
|
3020
3602
|
return `${functionKey}:${name}`;
|
|
@@ -3403,7 +3985,7 @@ export function checkWorkflowSystemAccessRule(input) {
|
|
|
3403
3985
|
* (`cli/src/lib/workflow-payload.ts`, #1177), both deleted with this change.
|
|
3404
3986
|
*/
|
|
3405
3987
|
const HANDLERS = "src/workflows/config/workflow-field-handlers.ts";
|
|
3406
|
-
const handler = (name) => handledBy(HANDLERS, name);
|
|
3988
|
+
const handler = (name) => handledBy(HANDLERS, name, "apply");
|
|
3407
3989
|
/**
|
|
3408
3990
|
* The save-time rules the server runs and the CLI does not (#3374). They need
|
|
3409
3991
|
* the runner's vocabulary (`isValidRunAs`, `GRANTABLE_SENSITIVE_CAPS`), which
|
|
@@ -3767,6 +4349,207 @@ export const WORKFLOW_SURFACE = {
|
|
|
3767
4349
|
"the durable runtime can execute (`delay`, `event.wait`)."),
|
|
3768
4350
|
],
|
|
3769
4351
|
};
|
|
4352
|
+
const FIELD_RANGES = [
|
|
4353
|
+
{ name: "minute", min: 0, max: 59 },
|
|
4354
|
+
{ name: "hour", min: 0, max: 23 },
|
|
4355
|
+
{ name: "day-of-month", min: 1, max: 31 },
|
|
4356
|
+
{ name: "month", min: 1, max: 12 },
|
|
4357
|
+
{ name: "day-of-week", min: 0, max: 6 },
|
|
4358
|
+
];
|
|
4359
|
+
function parseField(field, min, max, fieldName) {
|
|
4360
|
+
const result = new Set();
|
|
4361
|
+
const parts = field.split(",");
|
|
4362
|
+
for (const partRaw of parts) {
|
|
4363
|
+
const part = partRaw.trim();
|
|
4364
|
+
if (!part) {
|
|
4365
|
+
throw new Error(`Invalid ${fieldName} field: empty segment`);
|
|
4366
|
+
}
|
|
4367
|
+
let step = 1;
|
|
4368
|
+
let rangePart = part;
|
|
4369
|
+
const slashIdx = part.indexOf("/");
|
|
4370
|
+
if (slashIdx !== -1) {
|
|
4371
|
+
rangePart = part.slice(0, slashIdx);
|
|
4372
|
+
const stepStr = part.slice(slashIdx + 1);
|
|
4373
|
+
const stepNum = Number(stepStr);
|
|
4374
|
+
if (!Number.isInteger(stepNum) || stepNum <= 0) {
|
|
4375
|
+
throw new Error(`Invalid ${fieldName} step: "${stepStr}"`);
|
|
4376
|
+
}
|
|
4377
|
+
step = stepNum;
|
|
4378
|
+
}
|
|
4379
|
+
let lo;
|
|
4380
|
+
let hi;
|
|
4381
|
+
if (rangePart === "*") {
|
|
4382
|
+
lo = min;
|
|
4383
|
+
hi = max;
|
|
4384
|
+
}
|
|
4385
|
+
else if (rangePart.includes("-")) {
|
|
4386
|
+
const [loStr, hiStr] = rangePart.split("-");
|
|
4387
|
+
lo = Number(loStr);
|
|
4388
|
+
hi = Number(hiStr);
|
|
4389
|
+
if (!Number.isInteger(lo) || !Number.isInteger(hi)) {
|
|
4390
|
+
throw new Error(`Invalid ${fieldName} range: "${rangePart}"`);
|
|
4391
|
+
}
|
|
4392
|
+
}
|
|
4393
|
+
else {
|
|
4394
|
+
const exact = Number(rangePart);
|
|
4395
|
+
if (!Number.isInteger(exact)) {
|
|
4396
|
+
throw new Error(`Invalid ${fieldName} value: "${rangePart}"`);
|
|
4397
|
+
}
|
|
4398
|
+
lo = exact;
|
|
4399
|
+
hi = exact;
|
|
4400
|
+
}
|
|
4401
|
+
// Normalize day-of-week 7 -> 0 (Sunday). We only do this for bare
|
|
4402
|
+
// values, not inside a range — `5-7` would otherwise become `5-0`
|
|
4403
|
+
// which then fails the `lo > hi` check with a confusing error. Users
|
|
4404
|
+
// who want Fri–Sun should write `5-0,0` or `5,6,0`; we reject 7 in
|
|
4405
|
+
// ranges outright with a clearer message.
|
|
4406
|
+
if (fieldName === "day-of-week") {
|
|
4407
|
+
const isRange = rangePart.includes("-");
|
|
4408
|
+
if (isRange && (lo === 7 || hi === 7)) {
|
|
4409
|
+
throw new Error(`Invalid day-of-week range "${rangePart}": 7 (Sunday) is only allowed as a single value, not inside a range. Use 0 for Sunday in ranges.`);
|
|
4410
|
+
}
|
|
4411
|
+
if (!isRange) {
|
|
4412
|
+
if (lo === 7)
|
|
4413
|
+
lo = 0;
|
|
4414
|
+
if (hi === 7)
|
|
4415
|
+
hi = 0;
|
|
4416
|
+
}
|
|
4417
|
+
}
|
|
4418
|
+
if (lo < min || hi > max || lo > hi) {
|
|
4419
|
+
throw new Error(`Invalid ${fieldName} range: ${lo}-${hi} (allowed ${min}-${max})`);
|
|
4420
|
+
}
|
|
4421
|
+
for (let v = lo; v <= hi; v += step) {
|
|
4422
|
+
result.add(v);
|
|
4423
|
+
}
|
|
4424
|
+
}
|
|
4425
|
+
return result;
|
|
4426
|
+
}
|
|
4427
|
+
export function parseCron(expr) {
|
|
4428
|
+
if (!expr || typeof expr !== "string") {
|
|
4429
|
+
throw new Error("Cron expression must be a non-empty string");
|
|
4430
|
+
}
|
|
4431
|
+
const fields = expr.trim().split(/\s+/);
|
|
4432
|
+
if (fields.length !== 5) {
|
|
4433
|
+
throw new Error(`Cron expression must have exactly 5 fields, got ${fields.length}: "${expr}"`);
|
|
4434
|
+
}
|
|
4435
|
+
const [minField, hourField, domField, monField, dowField] = fields;
|
|
4436
|
+
const minutes = parseField(minField, FIELD_RANGES[0].min, FIELD_RANGES[0].max, "minute");
|
|
4437
|
+
const hours = parseField(hourField, FIELD_RANGES[1].min, FIELD_RANGES[1].max, "hour");
|
|
4438
|
+
const daysOfMonth = parseField(domField, FIELD_RANGES[2].min, FIELD_RANGES[2].max, "day-of-month");
|
|
4439
|
+
const months = parseField(monField, FIELD_RANGES[3].min, FIELD_RANGES[3].max, "month");
|
|
4440
|
+
const daysOfWeek = parseField(dowField, FIELD_RANGES[4].min, FIELD_RANGES[4].max, "day-of-week");
|
|
4441
|
+
return {
|
|
4442
|
+
minutes,
|
|
4443
|
+
hours,
|
|
4444
|
+
daysOfMonth,
|
|
4445
|
+
months,
|
|
4446
|
+
daysOfWeek,
|
|
4447
|
+
dayOfMonthWild: domField === "*",
|
|
4448
|
+
dayOfWeekWild: dowField === "*",
|
|
4449
|
+
};
|
|
4450
|
+
}
|
|
4451
|
+
// ─── The two predicates both runners call ───────────────────────────────
|
|
4452
|
+
/**
|
|
4453
|
+
* The message `parseCron` would throw for `expr`, or null when it parses.
|
|
4454
|
+
*
|
|
4455
|
+
* The same grammar, asked as a question instead of as an exception, because
|
|
4456
|
+
* both callers want the sentence rather than the control flow: the CLI's push
|
|
4457
|
+
* preflight collects one message per broken entry, and the standalone cron
|
|
4458
|
+
* routes render theirs into a 400 body. `expr` is typed `unknown` because it
|
|
4459
|
+
* arrives as an authored TOML value, where it can be anything at all.
|
|
4460
|
+
*/
|
|
4461
|
+
export function cronExpressionError(expr) {
|
|
4462
|
+
try {
|
|
4463
|
+
parseCron(expr);
|
|
4464
|
+
return null;
|
|
4465
|
+
}
|
|
4466
|
+
catch (err) {
|
|
4467
|
+
return err?.message || String(err);
|
|
4468
|
+
}
|
|
4469
|
+
}
|
|
4470
|
+
/**
|
|
4471
|
+
* Whether `Intl.DateTimeFormat` accepts `name` as a timezone.
|
|
4472
|
+
*
|
|
4473
|
+
* This is THE timezone check — before #3376 the identical probe was written
|
|
4474
|
+
* out three times (the function push boundary and the standalone create and
|
|
4475
|
+
* update). Each of those sites keeps its own sentence and its own defaulting;
|
|
4476
|
+
* what they now share is this answer. A non-string is false rather than a
|
|
4477
|
+
* throw, for the same reason as above.
|
|
4478
|
+
*/
|
|
4479
|
+
export function isValidTimezone(name) {
|
|
4480
|
+
try {
|
|
4481
|
+
new Intl.DateTimeFormat("en-US", { timeZone: name });
|
|
4482
|
+
return true;
|
|
4483
|
+
}
|
|
4484
|
+
catch {
|
|
4485
|
+
return false;
|
|
4486
|
+
}
|
|
4487
|
+
}
|
|
4488
|
+
// ── src/config-surface/cron-rules.ts ─────────────────────────────────────
|
|
4489
|
+
/**
|
|
4490
|
+
* The cron schedule rules two runners share (#3376, project
|
|
4491
|
+
* `cli-server-rule-parity` phase 4; intent §Success criteria 6).
|
|
4492
|
+
*
|
|
4493
|
+
* ── One implementation, two runners ──────────────────────────────────────
|
|
4494
|
+
*
|
|
4495
|
+
* A cron expression the grammar refuses, and a timezone `Intl` does not know,
|
|
4496
|
+
* are both decidable from the authored file. Before this issue only the SERVER
|
|
4497
|
+
* decided them, inside the apply loop: a `functions/<key>.toml` carrying
|
|
4498
|
+
* either passed `config push`'s preflight, sibling entities were written, and
|
|
4499
|
+
* the function alone was refused — while the command promises that a refusal
|
|
4500
|
+
* means nothing was applied. Now `cli/src/lib/function-sync.ts` and
|
|
4501
|
+
* `src/services/function-trigger-validation.ts` both call the rule below, and
|
|
4502
|
+
* the sentence an author reads is the same on either side of the wire because
|
|
4503
|
+
* it is built in one place.
|
|
4504
|
+
*
|
|
4505
|
+
* ── Why this is a `*-rules.ts` module and the parser is not ──────────────
|
|
4506
|
+
*
|
|
4507
|
+
* The registry's unclaimed-export check (`findUnclaimedRuleModuleExports`)
|
|
4508
|
+
* keys on this name under `src/config-surface/`: every exported function here
|
|
4509
|
+
* is a rule, so one no surface declares fails `cli-unit-tests`. The anchor
|
|
4510
|
+
* also forbids a `throw` outside the one refusal constructor — and `parseCron`
|
|
4511
|
+
* throws eight different sentences, which `tests/unit/cron-parser.test.ts`
|
|
4512
|
+
* pins. So the grammar and the `Intl` probe live next door in
|
|
4513
|
+
* `cron-expression.ts`, which is a shared-predicate module rather than a rules
|
|
4514
|
+
* module, and this one only decides and renders.
|
|
4515
|
+
*
|
|
4516
|
+
* ── What is NOT shared ───────────────────────────────────────────────────
|
|
4517
|
+
*
|
|
4518
|
+
* The standalone cron routes ask the same two predicates and render their own
|
|
4519
|
+
* `Invalid cron expression: …` / `Invalid timezone: …`, with their own
|
|
4520
|
+
* defaulting (create reads an absent timezone as UTC, update does not). That
|
|
4521
|
+
* is intent assumption 2 in the form its own fallback describes, and the shape
|
|
4522
|
+
* `workflow-rules.ts` set in phase 2: one predicate, each site's message and
|
|
4523
|
+
* defaulting preserved. No refusal message changed anywhere.
|
|
4524
|
+
*/
|
|
4525
|
+
/** The rule ids this module owns — the union type below anchors them (#3373). */
|
|
4526
|
+
export const CRON_TRIGGER_RULE_IDS = [
|
|
4527
|
+
"cron-trigger-expression",
|
|
4528
|
+
"cron-trigger-timezone",
|
|
4529
|
+
];
|
|
4530
|
+
/** The one refusal constructor: a new branch cannot compile without an id. */
|
|
4531
|
+
function refuseCronTriggerRule(ruleId, message) {
|
|
4532
|
+
return { ruleId, message };
|
|
4533
|
+
}
|
|
4534
|
+
/**
|
|
4535
|
+
* One `[[function.triggers.cron]]` entry's schedule, or null.
|
|
4536
|
+
*
|
|
4537
|
+
* Expression first, then timezone, which is the order the push boundary ran
|
|
4538
|
+
* them in — so an entry broken both ways still answers with the expression's
|
|
4539
|
+
* refusal, exactly as before. An unset or empty `timezone` reads as UTC, again
|
|
4540
|
+
* exactly as before: the boundary's `entry.timezone || "UTC"`.
|
|
4541
|
+
*/
|
|
4542
|
+
export function checkCronTriggerEntry(entry) {
|
|
4543
|
+
const cronError = cronExpressionError(entry.cron);
|
|
4544
|
+
if (cronError !== null) {
|
|
4545
|
+
return refuseCronTriggerRule("cron-trigger-expression", `cron trigger '${entry.name}': invalid cron expression — ${cronError}`);
|
|
4546
|
+
}
|
|
4547
|
+
const timezone = entry.timezone || "UTC";
|
|
4548
|
+
if (!isValidTimezone(timezone)) {
|
|
4549
|
+
return refuseCronTriggerRule("cron-trigger-timezone", `cron trigger '${entry.name}': invalid timezone '${timezone}'`);
|
|
4550
|
+
}
|
|
4551
|
+
return null;
|
|
4552
|
+
}
|
|
3770
4553
|
// ── src/config-surface/prompt-reasoning.ts ───────────────────────────────
|
|
3771
4554
|
/**
|
|
3772
4555
|
* The prompt config's reasoning controls, resolved onto each provider's own
|
|
@@ -4034,7 +4817,7 @@ export const PROMPT_SURFACE = {
|
|
|
4034
4817
|
type: "json",
|
|
4035
4818
|
emit: "whenSet",
|
|
4036
4819
|
writableOn: BOTH,
|
|
4037
|
-
validation: handledBy(PROMPT_HANDLER, "AdminAPI"),
|
|
4820
|
+
validation: handledBy(PROMPT_HANDLER, "AdminAPI", "apply"),
|
|
4038
4821
|
},
|
|
4039
4822
|
// #2644 — accepted by both handlers and returned by the detail, but
|
|
4040
4823
|
// never written to TOML before this: a prompt's declared output shape
|
|
@@ -4045,7 +4828,7 @@ export const PROMPT_SURFACE = {
|
|
|
4045
4828
|
type: "json",
|
|
4046
4829
|
emit: "whenSet",
|
|
4047
4830
|
writableOn: BOTH,
|
|
4048
|
-
validation: handledBy(PROMPT_HANDLER, "AdminAPI"),
|
|
4831
|
+
validation: handledBy(PROMPT_HANDLER, "AdminAPI", "apply"),
|
|
4049
4832
|
},
|
|
4050
4833
|
// #2652 — the CEL rule gating who may execute the prompt. Required at
|
|
4051
4834
|
// create; an explicit null/"" on update clears it (fail-closed). This
|
|
@@ -4058,7 +4841,7 @@ export const PROMPT_SURFACE = {
|
|
|
4058
4841
|
type: "string",
|
|
4059
4842
|
emit: "whenSet",
|
|
4060
4843
|
writableOn: BOTH,
|
|
4061
|
-
validation: handledBy(PROMPT_HANDLER, "AdminAPI"),
|
|
4844
|
+
validation: handledBy(PROMPT_HANDLER, "AdminAPI", "apply"),
|
|
4062
4845
|
},
|
|
4063
4846
|
],
|
|
4064
4847
|
notExposed: {
|
|
@@ -4160,7 +4943,7 @@ export const PROMPT_SURFACE = {
|
|
|
4160
4943
|
emit: "always",
|
|
4161
4944
|
writableOn: BOTH,
|
|
4162
4945
|
// Renamable on update, uniqueness-checked within the prompt.
|
|
4163
|
-
validation: handledBy(PROMPT_HANDLER, "AdminAPI"),
|
|
4946
|
+
validation: handledBy(PROMPT_HANDLER, "AdminAPI", "apply"),
|
|
4164
4947
|
},
|
|
4165
4948
|
// #2645 — a config's status is configuration: `prompts configs update
|
|
4166
4949
|
// --status archived` used to be the only way to retire one, and pull
|
|
@@ -4183,7 +4966,7 @@ export const PROMPT_SURFACE = {
|
|
|
4183
4966
|
type: "string",
|
|
4184
4967
|
emit: "whenSet",
|
|
4185
4968
|
writableOn: UPDATE_ONLY,
|
|
4186
|
-
validation: handledBy(PROMPT_HANDLER, "AdminAPI"),
|
|
4969
|
+
validation: handledBy(PROMPT_HANDLER, "AdminAPI", "apply"),
|
|
4187
4970
|
serverDefault: "active",
|
|
4188
4971
|
},
|
|
4189
4972
|
{
|
|
@@ -4201,7 +4984,7 @@ export const PROMPT_SURFACE = {
|
|
|
4201
4984
|
emit: "always",
|
|
4202
4985
|
writableOn: BOTH,
|
|
4203
4986
|
// openrouter | gemini, lower-cased and cross-checked against the model.
|
|
4204
|
-
validation: handledBy(PROMPT_HANDLER, "AdminAPI"),
|
|
4987
|
+
validation: handledBy(PROMPT_HANDLER, "AdminAPI", "apply"),
|
|
4205
4988
|
},
|
|
4206
4989
|
{
|
|
4207
4990
|
field: "model",
|
|
@@ -4209,7 +4992,7 @@ export const PROMPT_SURFACE = {
|
|
|
4209
4992
|
type: "string",
|
|
4210
4993
|
emit: "always",
|
|
4211
4994
|
writableOn: BOTH,
|
|
4212
|
-
validation: handledBy(PROMPT_HANDLER, "AdminAPI"),
|
|
4995
|
+
validation: handledBy(PROMPT_HANDLER, "AdminAPI", "apply"),
|
|
4213
4996
|
},
|
|
4214
4997
|
{
|
|
4215
4998
|
field: "systemPrompt",
|
|
@@ -4219,7 +5002,7 @@ export const PROMPT_SURFACE = {
|
|
|
4219
5002
|
writableOn: BOTH,
|
|
4220
5003
|
// Offloaded to R2 above the inline size limit and read back
|
|
4221
5004
|
// transparently, so the TOML value is the whole prompt either way.
|
|
4222
|
-
validation: handledBy("src/services/large-field-storage.ts", "storeField"),
|
|
5005
|
+
validation: handledBy("src/services/large-field-storage.ts", "storeField", "apply"),
|
|
4223
5006
|
},
|
|
4224
5007
|
{
|
|
4225
5008
|
field: "userPromptTemplate",
|
|
@@ -4227,7 +5010,7 @@ export const PROMPT_SURFACE = {
|
|
|
4227
5010
|
type: "string",
|
|
4228
5011
|
emit: "whenSet",
|
|
4229
5012
|
writableOn: BOTH,
|
|
4230
|
-
validation: handledBy("src/services/large-field-storage.ts", "storeField"),
|
|
5013
|
+
validation: handledBy("src/services/large-field-storage.ts", "storeField", "apply"),
|
|
4231
5014
|
},
|
|
4232
5015
|
{
|
|
4233
5016
|
field: "temperature",
|
|
@@ -4236,7 +5019,7 @@ export const PROMPT_SURFACE = {
|
|
|
4236
5019
|
emit: "whenSet",
|
|
4237
5020
|
writableOn: BOTH,
|
|
4238
5021
|
// Stored as a string on the model, returned parsed as a number.
|
|
4239
|
-
validation: handledBy(PROMPT_HANDLER, "AdminAPI"),
|
|
5022
|
+
validation: handledBy(PROMPT_HANDLER, "AdminAPI", "apply"),
|
|
4240
5023
|
dualEncoded: {
|
|
4241
5024
|
note: "#2869 — a `StringField` on the model: the server stores what it " +
|
|
4242
5025
|
"is given and returns `parseFloat` of it, so `temperature = " +
|
|
@@ -4252,7 +5035,7 @@ export const PROMPT_SURFACE = {
|
|
|
4252
5035
|
type: "number",
|
|
4253
5036
|
emit: "whenSet",
|
|
4254
5037
|
writableOn: BOTH,
|
|
4255
|
-
validation: handledBy(PROMPT_HANDLER, "AdminAPI"),
|
|
5038
|
+
validation: handledBy(PROMPT_HANDLER, "AdminAPI", "apply"),
|
|
4256
5039
|
dualEncoded: {
|
|
4257
5040
|
note: "#2869 — a `StringField` returned parsed, exactly as `temperature`.",
|
|
4258
5041
|
},
|
|
@@ -4281,7 +5064,7 @@ export const PROMPT_SURFACE = {
|
|
|
4281
5064
|
type: "json",
|
|
4282
5065
|
emit: "whenSet",
|
|
4283
5066
|
writableOn: BOTH,
|
|
4284
|
-
validation: handledBy(PROMPT_HANDLER, "AdminAPI"),
|
|
5067
|
+
validation: handledBy(PROMPT_HANDLER, "AdminAPI", "apply"),
|
|
4285
5068
|
},
|
|
4286
5069
|
{
|
|
4287
5070
|
field: "providerConfig",
|
|
@@ -4289,7 +5072,7 @@ export const PROMPT_SURFACE = {
|
|
|
4289
5072
|
type: "json",
|
|
4290
5073
|
emit: "whenSet",
|
|
4291
5074
|
writableOn: BOTH,
|
|
4292
|
-
validation: handledBy(PROMPT_HANDLER, "AdminAPI"),
|
|
5075
|
+
validation: handledBy(PROMPT_HANDLER, "AdminAPI", "apply"),
|
|
4293
5076
|
},
|
|
4294
5077
|
// #3358 — the provider's reasoning budget, in a provider-neutral
|
|
4295
5078
|
// spelling. Before this the only lever was the model itself: an app
|
|
@@ -4309,7 +5092,7 @@ export const PROMPT_SURFACE = {
|
|
|
4309
5092
|
type: "string",
|
|
4310
5093
|
emit: "whenSet",
|
|
4311
5094
|
writableOn: BOTH,
|
|
4312
|
-
validation: handledBy("src/config-surface/prompt-reasoning.ts", "resolvePromptReasoning"),
|
|
5095
|
+
validation: handledBy("src/config-surface/prompt-reasoning.ts", "resolvePromptReasoning", "apply"),
|
|
4313
5096
|
},
|
|
4314
5097
|
{
|
|
4315
5098
|
field: "reasoningBudget",
|
|
@@ -4317,7 +5100,7 @@ export const PROMPT_SURFACE = {
|
|
|
4317
5100
|
type: "number",
|
|
4318
5101
|
emit: "whenSet",
|
|
4319
5102
|
writableOn: BOTH,
|
|
4320
|
-
validation: handledBy("src/config-surface/prompt-reasoning.ts", "resolvePromptReasoning"),
|
|
5103
|
+
validation: handledBy("src/config-surface/prompt-reasoning.ts", "resolvePromptReasoning", "apply"),
|
|
4321
5104
|
},
|
|
4322
5105
|
],
|
|
4323
5106
|
notExposed: {
|
|
@@ -4440,7 +5223,7 @@ export const INTEGRATION_SURFACE = {
|
|
|
4440
5223
|
type: "number",
|
|
4441
5224
|
emit: "always",
|
|
4442
5225
|
writableOn: BOTH,
|
|
4443
|
-
validation: handledBy(INTEGRATION_HANDLER, "AdminAPI"),
|
|
5226
|
+
validation: handledBy(INTEGRATION_HANDLER, "AdminAPI", "apply"),
|
|
4444
5227
|
// `src/admin-api.ts` createAppIntegration: a create with no
|
|
4445
5228
|
// `timeoutMs` stores 300000. Recorded so a hand-authored file that
|
|
4446
5229
|
// omits the key compares (and pushes) as the value the server holds
|
|
@@ -4456,7 +5239,7 @@ export const INTEGRATION_SURFACE = {
|
|
|
4456
5239
|
type: "number",
|
|
4457
5240
|
emit: "whenSet",
|
|
4458
5241
|
writableOn: BOTH,
|
|
4459
|
-
validation: handledBy(INTEGRATION_HANDLER, "AdminAPI"),
|
|
5242
|
+
validation: handledBy(INTEGRATION_HANDLER, "AdminAPI", "apply"),
|
|
4460
5243
|
// Same create default, same reason (#2880 DSO-002): 1 MiB.
|
|
4461
5244
|
serverDefault: 1048576,
|
|
4462
5245
|
},
|
|
@@ -4468,7 +5251,7 @@ export const INTEGRATION_SURFACE = {
|
|
|
4468
5251
|
type: "string",
|
|
4469
5252
|
emit: "whenSet",
|
|
4470
5253
|
writableOn: BOTH,
|
|
4471
|
-
validation: handledBy(INTEGRATION_HANDLER, "AdminAPI"),
|
|
5254
|
+
validation: handledBy(INTEGRATION_HANDLER, "AdminAPI", "apply"),
|
|
4472
5255
|
},
|
|
4473
5256
|
],
|
|
4474
5257
|
notExposed: {
|
|
@@ -4633,7 +5416,7 @@ export const WEBHOOK_SURFACE = {
|
|
|
4633
5416
|
type: "string",
|
|
4634
5417
|
emit: "always",
|
|
4635
5418
|
writableOn: BOTH,
|
|
4636
|
-
validation: handledBy(WEBHOOK_HANDLER, "AdminAPI"),
|
|
5419
|
+
validation: handledBy(WEBHOOK_HANDLER, "AdminAPI", "apply"),
|
|
4637
5420
|
},
|
|
4638
5421
|
{
|
|
4639
5422
|
field: "toleranceSeconds",
|
|
@@ -4641,7 +5424,7 @@ export const WEBHOOK_SURFACE = {
|
|
|
4641
5424
|
type: "number",
|
|
4642
5425
|
emit: "always",
|
|
4643
5426
|
writableOn: BOTH,
|
|
4644
|
-
validation: handledBy(WEBHOOK_CONFIG_VALIDATION, "validateReplayProtection"),
|
|
5427
|
+
validation: handledBy(WEBHOOK_CONFIG_VALIDATION, "validateReplayProtection", "apply"),
|
|
4645
5428
|
},
|
|
4646
5429
|
{
|
|
4647
5430
|
field: "deduplicationEnabled",
|
|
@@ -4649,7 +5432,7 @@ export const WEBHOOK_SURFACE = {
|
|
|
4649
5432
|
type: "boolean",
|
|
4650
5433
|
emit: "always",
|
|
4651
5434
|
writableOn: BOTH,
|
|
4652
|
-
validation: handledBy(WEBHOOK_CONFIG_VALIDATION, "validateReplayProtection"),
|
|
5435
|
+
validation: handledBy(WEBHOOK_CONFIG_VALIDATION, "validateReplayProtection", "apply"),
|
|
4653
5436
|
},
|
|
4654
5437
|
{
|
|
4655
5438
|
field: "deduplicationWindowMs",
|
|
@@ -4657,7 +5440,7 @@ export const WEBHOOK_SURFACE = {
|
|
|
4657
5440
|
type: "number",
|
|
4658
5441
|
emit: "always",
|
|
4659
5442
|
writableOn: BOTH,
|
|
4660
|
-
validation: handledBy(WEBHOOK_CONFIG_VALIDATION, "validateReplayProtection"),
|
|
5443
|
+
validation: handledBy(WEBHOOK_CONFIG_VALIDATION, "validateReplayProtection", "apply"),
|
|
4661
5444
|
},
|
|
4662
5445
|
// Per-webhook inbound body cap. Unset means the platform default
|
|
4663
5446
|
// (5 MiB), so it is omitted from TOML rather than written as a zero.
|
|
@@ -4667,7 +5450,7 @@ export const WEBHOOK_SURFACE = {
|
|
|
4667
5450
|
type: "number",
|
|
4668
5451
|
emit: "whenSet",
|
|
4669
5452
|
writableOn: BOTH,
|
|
4670
|
-
validation: handledBy(WEBHOOK_CONFIG_VALIDATION, "validateMaxBodyBytes"),
|
|
5453
|
+
validation: handledBy(WEBHOOK_CONFIG_VALIDATION, "validateMaxBodyBytes", "apply"),
|
|
4671
5454
|
},
|
|
4672
5455
|
{
|
|
4673
5456
|
field: "secretGracePeriodMs",
|
|
@@ -4675,7 +5458,7 @@ export const WEBHOOK_SURFACE = {
|
|
|
4675
5458
|
type: "number",
|
|
4676
5459
|
emit: "always",
|
|
4677
5460
|
writableOn: BOTH,
|
|
4678
|
-
validation: handledBy(WEBHOOK_CONFIG_VALIDATION, "normalizeSecretGracePeriodMs"),
|
|
5461
|
+
validation: handledBy(WEBHOOK_CONFIG_VALIDATION, "normalizeSecretGracePeriodMs", "apply"),
|
|
4679
5462
|
},
|
|
4680
5463
|
// #2254 — reference-only: a whole `{{secrets.KEY}}` reference naming an
|
|
4681
5464
|
// existing app secret, and nothing else. The server never returns
|
|
@@ -4687,7 +5470,7 @@ export const WEBHOOK_SURFACE = {
|
|
|
4687
5470
|
type: "string",
|
|
4688
5471
|
emit: "whenSet",
|
|
4689
5472
|
writableOn: BOTH,
|
|
4690
|
-
validation: handledBy(WEBHOOK_SIGNING_SECRET_RULE, "validateWholeSecretReference"),
|
|
5473
|
+
validation: handledBy(WEBHOOK_SIGNING_SECRET_RULE, "validateWholeSecretReference", "apply"),
|
|
4691
5474
|
},
|
|
4692
5475
|
],
|
|
4693
5476
|
notExposed: {
|
|
@@ -4986,7 +5769,23 @@ export const CRON_TRIGGER_SURFACE = {
|
|
|
4986
5769
|
type: "string",
|
|
4987
5770
|
emit: "always",
|
|
4988
5771
|
writableOn: BOTH,
|
|
4989
|
-
|
|
5772
|
+
// #3376 — re-pointed from `src/cron-parser.ts`, which now only
|
|
5773
|
+
// re-exports the parser: the drift guard resolves `export function
|
|
5774
|
+
// <name>` in the named module's OWN source, so the declaring module
|
|
5775
|
+
// is the only one it can name.
|
|
5776
|
+
//
|
|
5777
|
+
// `apply`, not `preflight`, even though the predicate is vendored
|
|
5778
|
+
// and the rule is decidable from the file: no CLI check reads a
|
|
5779
|
+
// standalone `cron-triggers/<key>.toml`'s VALUES.
|
|
5780
|
+
// `collectConfigFileValidationErrors` is definition-driven and
|
|
5781
|
+
// cannot dispatch through a `handledBy` string (#1976), so this
|
|
5782
|
+
// file still fails at apply with the controller's message.
|
|
5783
|
+
// `preflight` would make the registry promise a refusal that in
|
|
5784
|
+
// fact lands mid-apply — and the reachability walker could not
|
|
5785
|
+
// catch the lie, because the FUNCTION leg reaches this same
|
|
5786
|
+
// predicate. Building that standalone leg, with its `config diff`
|
|
5787
|
+
// twin, is a finding for #3379.
|
|
5788
|
+
validation: handledBy("src/config-surface/cron-expression.ts", "cronExpressionError", "apply"),
|
|
4990
5789
|
},
|
|
4991
5790
|
{
|
|
4992
5791
|
field: "timezone",
|
|
@@ -4994,8 +5793,12 @@ export const CRON_TRIGGER_SURFACE = {
|
|
|
4994
5793
|
type: "string",
|
|
4995
5794
|
emit: "always",
|
|
4996
5795
|
writableOn: BOTH,
|
|
4997
|
-
// Validated against Intl; unset reads as "UTC".
|
|
4998
|
-
|
|
5796
|
+
// Validated against Intl; unset reads as "UTC". #3376 named the
|
|
5797
|
+
// predicate itself rather than the controller class: the probe the
|
|
5798
|
+
// three sites used to spell out is one function now, and this is
|
|
5799
|
+
// one of the two surfaces that own it. `apply` for the same reason
|
|
5800
|
+
// as `cron` above — no standalone CLI preflight runs it (#3379).
|
|
5801
|
+
validation: handledBy("src/config-surface/cron-expression.ts", "isValidTimezone", "apply"),
|
|
4999
5802
|
// `cron-triggers-controller.ts` stores `timezone || "UTC"`, so a file
|
|
5000
5803
|
// that omits the key describes a trigger running in UTC — compared
|
|
5001
5804
|
// and pushed as that, rather than as a difference no push can clear
|
|
@@ -5009,7 +5812,7 @@ export const CRON_TRIGGER_SURFACE = {
|
|
|
5009
5812
|
emit: "always",
|
|
5010
5813
|
writableOn: BOTH,
|
|
5011
5814
|
// #1002 — a reserved `__internal.` key is rejected at save time.
|
|
5012
|
-
validation: handledBy(CRON_TRIGGER_HANDLER, "CronTriggersController"),
|
|
5815
|
+
validation: handledBy(CRON_TRIGGER_HANDLER, "CronTriggersController", "apply"),
|
|
5013
5816
|
},
|
|
5014
5817
|
{
|
|
5015
5818
|
field: "overlapPolicy",
|
|
@@ -5018,7 +5821,7 @@ export const CRON_TRIGGER_SURFACE = {
|
|
|
5018
5821
|
emit: "always",
|
|
5019
5822
|
writableOn: BOTH,
|
|
5020
5823
|
// skip | allow.
|
|
5021
|
-
validation: handledBy(CRON_TRIGGER_HANDLER, "CronTriggersController"),
|
|
5824
|
+
validation: handledBy(CRON_TRIGGER_HANDLER, "CronTriggersController", "apply"),
|
|
5022
5825
|
// Same store-time default as `timezone` (#2880 DSO-002).
|
|
5023
5826
|
serverDefault: "skip",
|
|
5024
5827
|
},
|
|
@@ -5222,7 +6025,7 @@ export const BLOB_BUCKET_SURFACE = {
|
|
|
5222
6025
|
emit: "always",
|
|
5223
6026
|
writableOn: CREATE_ONLY,
|
|
5224
6027
|
// Immutable after create: `update` never reads it.
|
|
5225
|
-
validation: handledBy(BLOB_BUCKET_HANDLER, "BlobBucketsController"),
|
|
6028
|
+
validation: handledBy(BLOB_BUCKET_HANDLER, "BlobBucketsController", "apply"),
|
|
5226
6029
|
},
|
|
5227
6030
|
{
|
|
5228
6031
|
field: "preset",
|
|
@@ -5232,7 +6035,7 @@ export const BLOB_BUCKET_SURFACE = {
|
|
|
5232
6035
|
writableOn: BOTH,
|
|
5233
6036
|
// #1020 — the honest access field. A rule-set-backed bucket reports
|
|
5234
6037
|
// "custom", which is not written back as a preset.
|
|
5235
|
-
validation: handledBy("src/app-api/resource-types/blob-bucket.ts", "resolveBucketPreset"),
|
|
6038
|
+
validation: handledBy("src/app-api/resource-types/blob-bucket.ts", "resolveBucketPreset", "apply"),
|
|
5236
6039
|
},
|
|
5237
6040
|
{
|
|
5238
6041
|
field: "ruleSetId",
|
|
@@ -5242,7 +6045,7 @@ export const BLOB_BUCKET_SURFACE = {
|
|
|
5242
6045
|
writableOn: BOTH,
|
|
5243
6046
|
// Existence-checked against AccessRuleSet; mutually exclusive with a
|
|
5244
6047
|
// preset change in the same request.
|
|
5245
|
-
validation: handledBy(BLOB_BUCKET_HANDLER, "BlobBucketsController"),
|
|
6048
|
+
validation: handledBy(BLOB_BUCKET_HANDLER, "BlobBucketsController", "apply"),
|
|
5246
6049
|
},
|
|
5247
6050
|
],
|
|
5248
6051
|
notExposed: {
|
|
@@ -5370,7 +6173,7 @@ export const EMAIL_TEMPLATE_SURFACE = {
|
|
|
5370
6173
|
emit: "always",
|
|
5371
6174
|
writableOn: BOTH,
|
|
5372
6175
|
// Size-capped and checked for unescaped `{{{var}}}` interpolation.
|
|
5373
|
-
validation: handledBy(EMAIL_TEMPLATE_HANDLER, "AdminAPI"),
|
|
6176
|
+
validation: handledBy(EMAIL_TEMPLATE_HANDLER, "AdminAPI", "apply"),
|
|
5374
6177
|
},
|
|
5375
6178
|
{
|
|
5376
6179
|
field: "htmlBody",
|
|
@@ -5378,7 +6181,7 @@ export const EMAIL_TEMPLATE_SURFACE = {
|
|
|
5378
6181
|
type: "string",
|
|
5379
6182
|
emit: "whenSet",
|
|
5380
6183
|
writableOn: BOTH,
|
|
5381
|
-
validation: handledBy(EMAIL_TEMPLATE_HANDLER, "AdminAPI"),
|
|
6184
|
+
validation: handledBy(EMAIL_TEMPLATE_HANDLER, "AdminAPI", "apply"),
|
|
5382
6185
|
},
|
|
5383
6186
|
{
|
|
5384
6187
|
field: "textBody",
|
|
@@ -5386,7 +6189,7 @@ export const EMAIL_TEMPLATE_SURFACE = {
|
|
|
5386
6189
|
type: "string",
|
|
5387
6190
|
emit: "whenSet",
|
|
5388
6191
|
writableOn: BOTH,
|
|
5389
|
-
validation: handledBy(EMAIL_TEMPLATE_HANDLER, "AdminAPI"),
|
|
6192
|
+
validation: handledBy(EMAIL_TEMPLATE_HANDLER, "AdminAPI", "apply"),
|
|
5390
6193
|
},
|
|
5391
6194
|
],
|
|
5392
6195
|
notExposed: {
|
|
@@ -5516,7 +6319,7 @@ export const DATABASE_TYPE_SURFACE = {
|
|
|
5516
6319
|
writableOn: CREATE_ONLY,
|
|
5517
6320
|
// The type's identity: required and `#`-free on create, taken from
|
|
5518
6321
|
// the URL on update.
|
|
5519
|
-
validation: handledBy(DB_TYPE_HANDLER, "DatabaseTypeConfigController"),
|
|
6322
|
+
validation: handledBy(DB_TYPE_HANDLER, "DatabaseTypeConfigController", "apply"),
|
|
5520
6323
|
},
|
|
5521
6324
|
{
|
|
5522
6325
|
field: "ruleSetId",
|
|
@@ -5526,7 +6329,7 @@ export const DATABASE_TYPE_SURFACE = {
|
|
|
5526
6329
|
writableOn: BOTH,
|
|
5527
6330
|
// Authored as a portable name and sent as an id; the server checks
|
|
5528
6331
|
// the set exists and has `resourceType: "database"`.
|
|
5529
|
-
validation: handledBy(DB_TYPE_HANDLER, "DatabaseTypeConfigController"),
|
|
6332
|
+
validation: handledBy(DB_TYPE_HANDLER, "DatabaseTypeConfigController", "apply"),
|
|
5530
6333
|
},
|
|
5531
6334
|
{
|
|
5532
6335
|
field: "metadataAccess",
|
|
@@ -5536,7 +6339,7 @@ export const DATABASE_TYPE_SURFACE = {
|
|
|
5536
6339
|
writableOn: BOTH,
|
|
5537
6340
|
// CEL, parsed before storing (#1336). The legacy `metadataAccess`
|
|
5538
6341
|
// spelling stays readable on push — see tomlOnlyKeys.
|
|
5539
|
-
validation: handledBy(DB_TYPE_HANDLER, "DatabaseTypeConfigController"),
|
|
6342
|
+
validation: handledBy(DB_TYPE_HANDLER, "DatabaseTypeConfigController", "apply"),
|
|
5540
6343
|
},
|
|
5541
6344
|
{
|
|
5542
6345
|
field: "defaultAccess",
|
|
@@ -5545,7 +6348,7 @@ export const DATABASE_TYPE_SURFACE = {
|
|
|
5545
6348
|
emit: "whenSet",
|
|
5546
6349
|
writableOn: BOTH,
|
|
5547
6350
|
// CEL, plus the shared `md`-shape allowlist (#1523).
|
|
5548
|
-
validation: handledBy(DB_TYPE_HANDLER, "DatabaseTypeConfigController"),
|
|
6351
|
+
validation: handledBy(DB_TYPE_HANDLER, "DatabaseTypeConfigController", "apply"),
|
|
5549
6352
|
},
|
|
5550
6353
|
{
|
|
5551
6354
|
field: "autoPopulatedFields",
|
|
@@ -5555,7 +6358,7 @@ export const DATABASE_TYPE_SURFACE = {
|
|
|
5555
6358
|
writableOn: BOTH,
|
|
5556
6359
|
// #750 — shorthand and verbose forms both accepted, normalized by
|
|
5557
6360
|
// `validateAndNormalizeAutoPopulatedFields`.
|
|
5558
|
-
validation: handledBy(DB_TYPE_HANDLER, "DatabaseTypeConfigController"),
|
|
6361
|
+
validation: handledBy(DB_TYPE_HANDLER, "DatabaseTypeConfigController", "apply"),
|
|
5559
6362
|
},
|
|
5560
6363
|
{
|
|
5561
6364
|
field: "timestamps",
|
|
@@ -5565,7 +6368,7 @@ export const DATABASE_TYPE_SURFACE = {
|
|
|
5565
6368
|
writableOn: BOTH,
|
|
5566
6369
|
// #748 — `{ create?, update?, models? }`, normalized by
|
|
5567
6370
|
// `validateAndNormalizeTimestamps`.
|
|
5568
|
-
validation: handledBy(DB_TYPE_HANDLER, "DatabaseTypeConfigController"),
|
|
6371
|
+
validation: handledBy(DB_TYPE_HANDLER, "DatabaseTypeConfigController", "apply"),
|
|
5569
6372
|
},
|
|
5570
6373
|
],
|
|
5571
6374
|
notExposed: {
|
|
@@ -5707,7 +6510,7 @@ export const RULE_SET_SURFACE = {
|
|
|
5707
6510
|
emit: "always",
|
|
5708
6511
|
writableOn: BOTH,
|
|
5709
6512
|
// Required, trimmed, and non-empty on both modes.
|
|
5710
|
-
validation: handledBy(RULE_SET_HANDLER, "RuleSetsController"),
|
|
6513
|
+
validation: handledBy(RULE_SET_HANDLER, "RuleSetsController", "apply"),
|
|
5711
6514
|
},
|
|
5712
6515
|
{
|
|
5713
6516
|
field: "resourceType",
|
|
@@ -5717,7 +6520,7 @@ export const RULE_SET_SURFACE = {
|
|
|
5717
6520
|
writableOn: CREATE_ONLY,
|
|
5718
6521
|
// Checked against `getValidResourceTypes()`, and singleton types
|
|
5719
6522
|
// (#1148) reject a second set. `update` never reads it.
|
|
5720
|
-
validation: handledBy(RULE_SET_HANDLER, "RuleSetsController"),
|
|
6523
|
+
validation: handledBy(RULE_SET_HANDLER, "RuleSetsController", "apply"),
|
|
5721
6524
|
},
|
|
5722
6525
|
{
|
|
5723
6526
|
field: "description",
|
|
@@ -5809,7 +6612,7 @@ export const GROUP_TYPE_CONFIG_SURFACE = {
|
|
|
5809
6612
|
writableOn: CREATE_ONLY,
|
|
5810
6613
|
// The config's identity: required and `#`-free on create, taken from
|
|
5811
6614
|
// the URL on update.
|
|
5812
|
-
validation: handledBy(GROUP_TYPE_HANDLER, "GroupTypeConfigController"),
|
|
6615
|
+
validation: handledBy(GROUP_TYPE_HANDLER, "GroupTypeConfigController", "apply"),
|
|
5813
6616
|
},
|
|
5814
6617
|
{
|
|
5815
6618
|
field: "ruleSetId",
|
|
@@ -5819,7 +6622,7 @@ export const GROUP_TYPE_CONFIG_SURFACE = {
|
|
|
5819
6622
|
writableOn: BOTH,
|
|
5820
6623
|
// Authored as a name, sent as an id; existence and resourceType are
|
|
5821
6624
|
// checked server-side, and an empty value clears the reference.
|
|
5822
|
-
validation: handledBy(GROUP_TYPE_HANDLER, "GroupTypeConfigController"),
|
|
6625
|
+
validation: handledBy(GROUP_TYPE_HANDLER, "GroupTypeConfigController", "apply"),
|
|
5823
6626
|
},
|
|
5824
6627
|
{
|
|
5825
6628
|
field: "autoAddCreator",
|
|
@@ -5829,7 +6632,7 @@ export const GROUP_TYPE_CONFIG_SURFACE = {
|
|
|
5829
6632
|
writableOn: BOTH,
|
|
5830
6633
|
// Defaults to `true` when absent on create, and is coerced with `!!`
|
|
5831
6634
|
// on update — not a stored-as-is passthrough.
|
|
5832
|
-
validation: handledBy(GROUP_TYPE_HANDLER, "GroupTypeConfigController"),
|
|
6635
|
+
validation: handledBy(GROUP_TYPE_HANDLER, "GroupTypeConfigController", "apply"),
|
|
5833
6636
|
},
|
|
5834
6637
|
],
|
|
5835
6638
|
notExposed: {
|
|
@@ -5902,7 +6705,7 @@ export const COLLECTION_TYPE_CONFIG_SURFACE = {
|
|
|
5902
6705
|
writableOn: CREATE_ONLY,
|
|
5903
6706
|
// The config's identity: required on create, taken from the URL on
|
|
5904
6707
|
// update.
|
|
5905
|
-
validation: handledBy(COLLECTION_TYPE_HANDLER, "CollectionTypeConfigController"),
|
|
6708
|
+
validation: handledBy(COLLECTION_TYPE_HANDLER, "CollectionTypeConfigController", "apply"),
|
|
5906
6709
|
},
|
|
5907
6710
|
{
|
|
5908
6711
|
field: "ruleSetId",
|
|
@@ -5912,7 +6715,7 @@ export const COLLECTION_TYPE_CONFIG_SURFACE = {
|
|
|
5912
6715
|
writableOn: BOTH,
|
|
5913
6716
|
// Authored as a name, sent as an id; existence and resourceType are
|
|
5914
6717
|
// checked server-side, and an empty value clears the reference.
|
|
5915
|
-
validation: handledBy(COLLECTION_TYPE_HANDLER, "CollectionTypeConfigController"),
|
|
6718
|
+
validation: handledBy(COLLECTION_TYPE_HANDLER, "CollectionTypeConfigController", "apply"),
|
|
5916
6719
|
},
|
|
5917
6720
|
],
|
|
5918
6721
|
notExposed: {
|
|
@@ -5977,6 +6780,18 @@ export const COLLECTION_TYPE_CONFIG_SURFACE = {
|
|
|
5977
6780
|
* enum and `unique` rules — but the field itself round-trips as one value.
|
|
5978
6781
|
*/
|
|
5979
6782
|
const METADATA_HANDLER = "src/app-api/controllers/metadata-controller.ts";
|
|
6783
|
+
/**
|
|
6784
|
+
* ── Why this module's CLI handler is declared `apply` (#3375) ─────────────
|
|
6785
|
+
*
|
|
6786
|
+
* A `preflight` handler must be VENDORED — its module lives under
|
|
6787
|
+
* `src/config-surface/`, so the CLI runs the server's own copy through the
|
|
6788
|
+
* generated artifact rather than a second implementation. A handler that
|
|
6789
|
+
* lives in `cli/src/` is a CLI-only copy of a server rule, which is precisely
|
|
6790
|
+
* the drift this project exists to remove; until it is vendored it cannot make
|
|
6791
|
+
* the registry's preflight promise, whatever the push path happens to do
|
|
6792
|
+
* today. Declared `apply`, and recorded as a finding for the validation child
|
|
6793
|
+
* (#3379) rather than papered over with a third stage.
|
|
6794
|
+
*/
|
|
5980
6795
|
const CLI_METADATA_TOML = "cli/src/lib/toml-metadata-config.ts";
|
|
5981
6796
|
export const METADATA_CATEGORY_CONFIG_SURFACE = {
|
|
5982
6797
|
label: "metadata-category-config",
|
|
@@ -5995,7 +6810,7 @@ export const METADATA_CATEGORY_CONFIG_SURFACE = {
|
|
|
5995
6810
|
emit: "always",
|
|
5996
6811
|
writableOn: BOTH,
|
|
5997
6812
|
// Half of the category's identity; required on every upsert.
|
|
5998
|
-
validation: handledBy(METADATA_HANDLER, "MetadataController"),
|
|
6813
|
+
validation: handledBy(METADATA_HANDLER, "MetadataController", "apply"),
|
|
5999
6814
|
},
|
|
6000
6815
|
{
|
|
6001
6816
|
field: "category",
|
|
@@ -6004,7 +6819,7 @@ export const METADATA_CATEGORY_CONFIG_SURFACE = {
|
|
|
6004
6819
|
emit: "always",
|
|
6005
6820
|
writableOn: BOTH,
|
|
6006
6821
|
// The other half of the identity; required on every upsert.
|
|
6007
|
-
validation: handledBy(METADATA_HANDLER, "MetadataController"),
|
|
6822
|
+
validation: handledBy(METADATA_HANDLER, "MetadataController", "apply"),
|
|
6008
6823
|
},
|
|
6009
6824
|
{
|
|
6010
6825
|
field: "readRule",
|
|
@@ -6013,7 +6828,7 @@ export const METADATA_CATEGORY_CONFIG_SURFACE = {
|
|
|
6013
6828
|
emit: "whenSet",
|
|
6014
6829
|
writableOn: BOTH,
|
|
6015
6830
|
// CEL, parsed and stored or nulled.
|
|
6016
|
-
validation: handledBy(METADATA_HANDLER, "MetadataController"),
|
|
6831
|
+
validation: handledBy(METADATA_HANDLER, "MetadataController", "apply"),
|
|
6017
6832
|
},
|
|
6018
6833
|
{
|
|
6019
6834
|
field: "writeRule",
|
|
@@ -6021,7 +6836,7 @@ export const METADATA_CATEGORY_CONFIG_SURFACE = {
|
|
|
6021
6836
|
type: "string",
|
|
6022
6837
|
emit: "whenSet",
|
|
6023
6838
|
writableOn: BOTH,
|
|
6024
|
-
validation: handledBy(METADATA_HANDLER, "MetadataController"),
|
|
6839
|
+
validation: handledBy(METADATA_HANDLER, "MetadataController", "apply"),
|
|
6025
6840
|
},
|
|
6026
6841
|
{
|
|
6027
6842
|
field: "description",
|
|
@@ -6041,7 +6856,7 @@ export const METADATA_CATEGORY_CONFIG_SURFACE = {
|
|
|
6041
6856
|
// `{ fields: {} }` so the file stays pushable. Field types, enums and
|
|
6042
6857
|
// the single-`unique` rule are checked locally before push and again
|
|
6043
6858
|
// server-side.
|
|
6044
|
-
validation: handledBy(CLI_METADATA_TOML, "validateCategorySchema"),
|
|
6859
|
+
validation: handledBy(CLI_METADATA_TOML, "validateCategorySchema", "apply"),
|
|
6045
6860
|
},
|
|
6046
6861
|
],
|
|
6047
6862
|
notExposed: {
|
|
@@ -6197,34 +7012,16 @@ const functionRules = (stage, modulePath, exportName, entries) => entries.map(([
|
|
|
6197
7012
|
stage,
|
|
6198
7013
|
handler: enforcedBy(modulePath, exportName),
|
|
6199
7014
|
}));
|
|
6200
|
-
/** `mode` / `durable`: one fact, two spellings, resolved once (#3281). */
|
|
6201
|
-
const FUNCTION_MODE_RULES = functionRules("preflight", "src/config-surface/function-mode.ts", "resolveDeclaredMode", [
|
|
6202
|
-
["mode-value", '`mode` must be exactly "request" or "task" when present.'],
|
|
6203
|
-
["durable-value", "`durable`, the accepted alias of `mode`, must be a boolean."],
|
|
6204
|
-
[
|
|
6205
|
-
"mode-durable-agreement",
|
|
6206
|
-
"A file carrying both `mode` and `durable` must have them agree; a " +
|
|
6207
|
-
"silent winner would be worse than two spellings.",
|
|
6208
|
-
],
|
|
6209
|
-
]);
|
|
6210
7015
|
/** The trigger blocks' shape rules — one id per refusal (#3181, #3184, #3334). */
|
|
6211
7016
|
const TRIGGER_DECLARATION_RULES = functionRules("preflight", "src/config-surface/trigger-declaration.ts", "normalizeTriggerDeclaration", [
|
|
6212
7017
|
["triggers-table", "`[function.triggers]` must be a table."],
|
|
6213
|
-
["trigger-kind", "Only `webhook
|
|
6214
|
-
[
|
|
6215
|
-
"database-trigger-mode",
|
|
6216
|
-
"A database trigger needs request mode: the fire runs inside the write " +
|
|
6217
|
-
"request, and a task run has no answer to give there.",
|
|
6218
|
-
],
|
|
6219
|
-
["database-trigger-cap", "A function watches at most five database types."],
|
|
6220
|
-
["database-trigger-entry", "Each `[[function.triggers.database]]` entry is a table."],
|
|
6221
|
-
["database-trigger-keys", "A database entry accepts only `type`."],
|
|
6222
|
-
["database-trigger-type-required", "A database entry names its `type`."],
|
|
7018
|
+
["trigger-kind", "Only `webhook` and `cron` are trigger kinds."],
|
|
6223
7019
|
[
|
|
6224
|
-
"database-trigger-
|
|
6225
|
-
"
|
|
7020
|
+
"database-trigger-removed",
|
|
7021
|
+
"`[[function.triggers.database]]` is refused: the trigger was removed " +
|
|
7022
|
+
"(#3483), and the refusal names the pattern that replaces it — do the " +
|
|
7023
|
+
"follow-up in the handler that writes, or start a task.",
|
|
6226
7024
|
],
|
|
6227
|
-
["database-trigger-duplicate", "Each database type is declared once."],
|
|
6228
7025
|
[
|
|
6229
7026
|
"webhook-trigger-single",
|
|
6230
7027
|
"`[function.triggers.webhook]` is one table, never an array: one webhook " +
|
|
@@ -6232,11 +7029,6 @@ const TRIGGER_DECLARATION_RULES = functionRules("preflight", "src/config-surface
|
|
|
6232
7029
|
],
|
|
6233
7030
|
["webhook-trigger-table", "`[function.triggers.webhook]` must be a table."],
|
|
6234
7031
|
["webhook-trigger-keys", "A webhook block accepts only its declared keys."],
|
|
6235
|
-
[
|
|
6236
|
-
"webhook-trigger-mode",
|
|
6237
|
-
"A webhook trigger needs request mode: the provider waits inside the " +
|
|
6238
|
-
"request for an answer a task run cannot give there.",
|
|
6239
|
-
],
|
|
6240
7032
|
["webhook-trigger-scheme-required", "A webhook block names its `verificationScheme`."],
|
|
6241
7033
|
[
|
|
6242
7034
|
"webhook-trigger-scheme-none",
|
|
@@ -6260,20 +7052,6 @@ const TRIGGER_DECLARATION_RULES = functionRules("preflight", "src/config-surface
|
|
|
6260
7052
|
["cron-trigger-duplicate", "Each cron name appears once within the function."],
|
|
6261
7053
|
["cron-trigger-expression-required", "A cron entry carries a `cron` expression."],
|
|
6262
7054
|
["cron-trigger-overlap-policy", "`overlapPolicy` is `skip` or `allow`."],
|
|
6263
|
-
[
|
|
6264
|
-
"cron-trigger-mode-value",
|
|
6265
|
-
"A cron entry's `mode` names one of the two runners: a fire runs on one " +
|
|
6266
|
-
"of them, so `any` is not a value it can take.",
|
|
6267
|
-
],
|
|
6268
|
-
[
|
|
6269
|
-
"cron-trigger-mode-required",
|
|
6270
|
-
"A cron entry on an `any` function says which runner each fire uses: " +
|
|
6271
|
-
"nothing else in the system answers it.",
|
|
6272
|
-
],
|
|
6273
|
-
[
|
|
6274
|
-
"cron-trigger-mode-locked",
|
|
6275
|
-
"A cron entry may not name the runner the function's own lock forbids.",
|
|
6276
|
-
],
|
|
6277
7055
|
]);
|
|
6278
7056
|
/**
|
|
6279
7057
|
* The file-decidable half of the webhook `signingSecret` rule (#3320). The
|
|
@@ -6297,33 +7075,35 @@ const SIGNING_SECRET_RULES = functionRules("preflight", "src/config-surface/webh
|
|
|
6297
7075
|
"`plaid`) or that verifies nothing (`none`) refuses a `signingSecret`.",
|
|
6298
7076
|
],
|
|
6299
7077
|
]);
|
|
6300
|
-
/**
|
|
6301
|
-
|
|
7078
|
+
/**
|
|
7079
|
+
* The cron entries' schedule, decidable from the file (#3376).
|
|
7080
|
+
*
|
|
7081
|
+
* Both were `apply` until this issue — declared where they ran, with a note
|
|
7082
|
+
* saying so — because the only implementation lived on the server. Splitting
|
|
7083
|
+
* `src/cron-parser.ts` put the grammar and the `Intl` probe under
|
|
7084
|
+
* `src/config-surface/`, so the CLI's push preflight runs the same rule
|
|
7085
|
+
* before the first request and a malformed schedule no longer reaches apply.
|
|
7086
|
+
*/
|
|
7087
|
+
const CRON_SCHEDULE_RULES = functionRules("preflight", "src/config-surface/cron-rules.ts", "checkCronTriggerEntry", [
|
|
6302
7088
|
[
|
|
6303
|
-
"
|
|
6304
|
-
"
|
|
6305
|
-
"
|
|
7089
|
+
"cron-trigger-expression",
|
|
7090
|
+
"The `cron` expression must parse under the server's five-field grammar " +
|
|
7091
|
+
"(`parseCron`).",
|
|
6306
7092
|
],
|
|
6307
7093
|
[
|
|
6308
|
-
"
|
|
6309
|
-
"
|
|
7094
|
+
"cron-trigger-timezone",
|
|
7095
|
+
"The `timezone`, unset reading as UTC, must be one " +
|
|
7096
|
+
"`Intl.DateTimeFormat` accepts.",
|
|
6310
7097
|
],
|
|
7098
|
+
]);
|
|
7099
|
+
/** What only the app's state can decide, at the push boundary (#3181). */
|
|
7100
|
+
const FUNCTION_TRIGGER_STATE_RULES = functionRules("apply", "src/services/function-trigger-validation.ts", "validateFunctionTriggerDeclaration", [
|
|
6311
7101
|
[
|
|
6312
7102
|
"webhook-trigger-settings",
|
|
6313
7103
|
"The webhook block's verification settings pass the same pipeline the " +
|
|
6314
7104
|
"standalone create runs (scheme config, replay protection, secret " +
|
|
6315
7105
|
"existence), prefixed `[function.triggers.webhook]: `.",
|
|
6316
7106
|
],
|
|
6317
|
-
[
|
|
6318
|
-
"cron-trigger-expression",
|
|
6319
|
-
"The `cron` expression must parse (`parseCron`). File-decidable; #3376 " +
|
|
6320
|
-
"moves it to preflight.",
|
|
6321
|
-
],
|
|
6322
|
-
[
|
|
6323
|
-
"cron-trigger-timezone",
|
|
6324
|
-
"The `timezone` must be one `Intl.DateTimeFormat` accepts. " +
|
|
6325
|
-
"File-decidable; #3376 moves it to preflight.",
|
|
6326
|
-
],
|
|
6327
7107
|
[
|
|
6328
7108
|
"webhook-trigger-key-held",
|
|
6329
7109
|
"A standalone webhook that already holds the function's key blocks the " +
|
|
@@ -6366,7 +7146,7 @@ export const SERVER_FUNCTION_SURFACE = {
|
|
|
6366
7146
|
// function with no gate fails closed at invoke, so accepting a bundle
|
|
6367
7147
|
// for one would report a successful push of something that can never
|
|
6368
7148
|
// run.
|
|
6369
|
-
validation: handledBy(FUNCTION_HANDLER, "ServerFunctionsController"),
|
|
7149
|
+
validation: handledBy(FUNCTION_HANDLER, "ServerFunctionsController", "apply"),
|
|
6370
7150
|
},
|
|
6371
7151
|
{
|
|
6372
7152
|
field: "inputSchema",
|
|
@@ -6374,7 +7154,7 @@ export const SERVER_FUNCTION_SURFACE = {
|
|
|
6374
7154
|
type: "json",
|
|
6375
7155
|
emit: "whenSet",
|
|
6376
7156
|
writableOn: BOTH,
|
|
6377
|
-
validation: handledBy(FUNCTION_HANDLER, "ServerFunctionsController"),
|
|
7157
|
+
validation: handledBy(FUNCTION_HANDLER, "ServerFunctionsController", "apply"),
|
|
6378
7158
|
},
|
|
6379
7159
|
{
|
|
6380
7160
|
field: "outputSchema",
|
|
@@ -6382,7 +7162,7 @@ export const SERVER_FUNCTION_SURFACE = {
|
|
|
6382
7162
|
type: "json",
|
|
6383
7163
|
emit: "whenSet",
|
|
6384
7164
|
writableOn: BOTH,
|
|
6385
|
-
validation: handledBy(FUNCTION_HANDLER, "ServerFunctionsController"),
|
|
7165
|
+
validation: handledBy(FUNCTION_HANDLER, "ServerFunctionsController", "apply"),
|
|
6386
7166
|
},
|
|
6387
7167
|
],
|
|
6388
7168
|
notExposed: {
|
|
@@ -6418,6 +7198,18 @@ export const SERVER_FUNCTION_SURFACE = {
|
|
|
6418
7198
|
"Repointed by a config-version push; versions are nameless, so " +
|
|
6419
7199
|
"there is no `activeConfigName` for an author to write.",
|
|
6420
7200
|
},
|
|
7201
|
+
activeConfigActivatedAt: {
|
|
7202
|
+
kind: "server-owned",
|
|
7203
|
+
note: "#3462 — WHEN the active version was put live. Stamped by both " +
|
|
7204
|
+
"doors that move the pointer (`config push` and the activate " +
|
|
7205
|
+
"route); an audit fact, so nothing authors it and `config pull` " +
|
|
7206
|
+
"does not write it back.",
|
|
7207
|
+
},
|
|
7208
|
+
activeConfigActivatedBy: {
|
|
7209
|
+
kind: "server-owned",
|
|
7210
|
+
note: "#3462 — WHICH admin put the active version live. Same two doors, " +
|
|
7211
|
+
"same reason: the actor is observed, never declared.",
|
|
7212
|
+
},
|
|
6421
7213
|
runAs: {
|
|
6422
7214
|
kind: "server-owned",
|
|
6423
7215
|
note: "RETIRED from the write surface by #3279: function code acts as " +
|
|
@@ -6462,26 +7254,24 @@ export const SERVER_FUNCTION_SURFACE = {
|
|
|
6462
7254
|
},
|
|
6463
7255
|
mode: {
|
|
6464
7256
|
kind: "structural",
|
|
6465
|
-
note:
|
|
6466
|
-
|
|
6467
|
-
"
|
|
6468
|
-
"
|
|
6469
|
-
|
|
6470
|
-
"
|
|
6471
|
-
"
|
|
6472
|
-
"
|
|
6473
|
-
"
|
|
7257
|
+
note: "RETIRED (#3482), and still ACCEPTED through phase 4. A function " +
|
|
7258
|
+
"is a function: the CALLER picks the runtime at each call, with " +
|
|
7259
|
+
"`invoke` running it inside the request and `start` running it as " +
|
|
7260
|
+
"a task, cron fires always starting a task run and webhook " +
|
|
7261
|
+
"deliveries always running as a request. A file that still " +
|
|
7262
|
+
"carries the key pushes, with a warning naming it as ignored and " +
|
|
7263
|
+
"every other key applied, and `config pull` removes it; from " +
|
|
7264
|
+
"phase 5 (#3188) it is refused as an unknown key. Nothing reads " +
|
|
7265
|
+
"it, and its value is not validated. A function that must not run " +
|
|
7266
|
+
"under one runtime says so in code with `assertRuntime`.",
|
|
6474
7267
|
},
|
|
6475
7268
|
durable: {
|
|
6476
7269
|
kind: "structural",
|
|
6477
|
-
note:
|
|
6478
|
-
|
|
6479
|
-
|
|
6480
|
-
|
|
6481
|
-
"
|
|
6482
|
-
"the property; `config pull` writes the authored bytes back " +
|
|
6483
|
-
"unchanged, so a file that says it keeps saying it until its " +
|
|
6484
|
-
"author edits it.",
|
|
7270
|
+
note: "RETIRED (#3482), and still ACCEPTED through phase 4 — the same " +
|
|
7271
|
+
"as `mode`, whose alias it was. It named one runner for the whole " +
|
|
7272
|
+
"function, and there is no such thing any more. A file that " +
|
|
7273
|
+
"carries it pushes with a warning, `config pull` removes it, and " +
|
|
7274
|
+
"phase 5 refuses it.",
|
|
6485
7275
|
},
|
|
6486
7276
|
limits: {
|
|
6487
7277
|
kind: "structural",
|
|
@@ -6524,9 +7314,11 @@ export const SERVER_FUNCTION_SURFACE = {
|
|
|
6524
7314
|
// id-bearing slot until #3375 gives `ConfigField.validation` a stage, and a
|
|
6525
7315
|
// module's list must equal its declarations in both directions.
|
|
6526
7316
|
crossFieldRules: [
|
|
6527
|
-
...FUNCTION_MODE_RULES,
|
|
6528
7317
|
...TRIGGER_DECLARATION_RULES,
|
|
6529
7318
|
...SIGNING_SECRET_RULES,
|
|
7319
|
+
// #3376 — after the signing-secret group, mirroring the order
|
|
7320
|
+
// `functionTomlPreflightErrors` runs them in.
|
|
7321
|
+
...CRON_SCHEDULE_RULES,
|
|
6530
7322
|
...FUNCTION_TRIGGER_STATE_RULES,
|
|
6531
7323
|
],
|
|
6532
7324
|
};
|
|
@@ -6555,6 +7347,18 @@ export const SERVER_FUNCTION_SURFACE = {
|
|
|
6555
7347
|
* re-stringifies on store.
|
|
6556
7348
|
*/
|
|
6557
7349
|
const TEST_CASE_HANDLER = "src/admin-api.ts";
|
|
7350
|
+
/**
|
|
7351
|
+
* ── Why this module's CLI handler is declared `apply` (#3375) ─────────────
|
|
7352
|
+
*
|
|
7353
|
+
* A `preflight` handler must be VENDORED — its module lives under
|
|
7354
|
+
* `src/config-surface/`, so the CLI runs the server's own copy through the
|
|
7355
|
+
* generated artifact rather than a second implementation. A handler that
|
|
7356
|
+
* lives in `cli/src/` is a CLI-only copy of a server rule, which is precisely
|
|
7357
|
+
* the drift this project exists to remove; until it is vendored it cannot make
|
|
7358
|
+
* the registry's preflight promise, whatever the push path happens to do
|
|
7359
|
+
* today. Declared `apply`, and recorded as a finding for the validation child
|
|
7360
|
+
* (#3379) rather than papered over with a third stage.
|
|
7361
|
+
*/
|
|
6558
7362
|
const CLI_SYNC = "cli/src/commands/sync.ts";
|
|
6559
7363
|
export const TEST_CASE_SURFACE = {
|
|
6560
7364
|
label: "test-case",
|
|
@@ -6572,7 +7376,7 @@ export const TEST_CASE_SURFACE = {
|
|
|
6572
7376
|
emit: "always",
|
|
6573
7377
|
writableOn: BOTH,
|
|
6574
7378
|
// Required and trimmed on create; an empty name is a 400.
|
|
6575
|
-
validation: handledBy(TEST_CASE_HANDLER, "AdminAPI"),
|
|
7379
|
+
validation: handledBy(TEST_CASE_HANDLER, "AdminAPI", "apply"),
|
|
6576
7380
|
},
|
|
6577
7381
|
{
|
|
6578
7382
|
field: "description",
|
|
@@ -6590,7 +7394,7 @@ export const TEST_CASE_SURFACE = {
|
|
|
6590
7394
|
writableOn: BOTH,
|
|
6591
7395
|
// Required. TOML carries the JSON text; the wire carries the parsed
|
|
6592
7396
|
// object, which the server re-stringifies for storage.
|
|
6593
|
-
validation: handledBy(CLI_SYNC, "parseTestCaseToml"),
|
|
7397
|
+
validation: handledBy(CLI_SYNC, "parseTestCaseToml", "apply"),
|
|
6594
7398
|
},
|
|
6595
7399
|
{
|
|
6596
7400
|
field: "configId",
|
|
@@ -6599,7 +7403,7 @@ export const TEST_CASE_SURFACE = {
|
|
|
6599
7403
|
emit: "always",
|
|
6600
7404
|
writableOn: BOTH,
|
|
6601
7405
|
// Authored as the config's NAME and resolved to an id at push time.
|
|
6602
|
-
validation: handledBy(CLI_SYNC, "parseTestCaseToml"),
|
|
7406
|
+
validation: handledBy(CLI_SYNC, "parseTestCaseToml", "apply"),
|
|
6603
7407
|
},
|
|
6604
7408
|
{
|
|
6605
7409
|
field: "evaluatorPromptId",
|
|
@@ -6608,7 +7412,7 @@ export const TEST_CASE_SURFACE = {
|
|
|
6608
7412
|
emit: "always",
|
|
6609
7413
|
writableOn: BOTH,
|
|
6610
7414
|
// Authored as the judge prompt's KEY and resolved to an id at push.
|
|
6611
|
-
validation: handledBy(CLI_SYNC, "parseTestCaseToml"),
|
|
7415
|
+
validation: handledBy(CLI_SYNC, "parseTestCaseToml", "apply"),
|
|
6612
7416
|
},
|
|
6613
7417
|
{
|
|
6614
7418
|
field: "evaluatorConfigId",
|
|
@@ -6616,7 +7420,7 @@ export const TEST_CASE_SURFACE = {
|
|
|
6616
7420
|
type: "string",
|
|
6617
7421
|
emit: "always",
|
|
6618
7422
|
writableOn: BOTH,
|
|
6619
|
-
validation: handledBy(CLI_SYNC, "parseTestCaseToml"),
|
|
7423
|
+
validation: handledBy(CLI_SYNC, "parseTestCaseToml", "apply"),
|
|
6620
7424
|
},
|
|
6621
7425
|
{
|
|
6622
7426
|
field: "expectedOutputPattern",
|
|
@@ -6633,7 +7437,7 @@ export const TEST_CASE_SURFACE = {
|
|
|
6633
7437
|
emit: "always",
|
|
6634
7438
|
writableOn: BOTH,
|
|
6635
7439
|
// JSON array of strings; TOML carries the JSON text.
|
|
6636
|
-
validation: handledBy(CLI_SYNC, "parseTestCaseToml"),
|
|
7440
|
+
validation: handledBy(CLI_SYNC, "parseTestCaseToml", "apply"),
|
|
6637
7441
|
},
|
|
6638
7442
|
{
|
|
6639
7443
|
field: "expectedJsonSubset",
|
|
@@ -6642,7 +7446,7 @@ export const TEST_CASE_SURFACE = {
|
|
|
6642
7446
|
emit: "always",
|
|
6643
7447
|
writableOn: BOTH,
|
|
6644
7448
|
// JSON object, deep-partial-matched against the parsed output.
|
|
6645
|
-
validation: handledBy(CLI_SYNC, "parseTestCaseToml"),
|
|
7449
|
+
validation: handledBy(CLI_SYNC, "parseTestCaseToml", "apply"),
|
|
6646
7450
|
},
|
|
6647
7451
|
],
|
|
6648
7452
|
notExposed: {
|
|
@@ -6836,7 +7640,7 @@ export const APP_SETTINGS_SURFACE = {
|
|
|
6836
7640
|
// with `=== true`, so `"false"` must be a 400 rather than a stored
|
|
6837
7641
|
// `true`. The generated request schema stays keys-only (#1976), so
|
|
6838
7642
|
// the check is `parseDirectLlmEnabledWrite` in the handler.
|
|
6839
|
-
validation: handledBy(SETTINGS_HANDLER, "SettingsController"),
|
|
7643
|
+
validation: handledBy(SETTINGS_HANDLER, "SettingsController", "apply"),
|
|
6840
7644
|
},
|
|
6841
7645
|
{
|
|
6842
7646
|
field: "allowedDomains",
|
|
@@ -6854,7 +7658,7 @@ export const APP_SETTINGS_SURFACE = {
|
|
|
6854
7658
|
writableOn: UPDATE_ONLY,
|
|
6855
7659
|
// `+primitivetest` bypass whitelist (#490/#675), shape-checked by
|
|
6856
7660
|
// `validateTestAccountBaseEmails`.
|
|
6857
|
-
validation: handledBy(SETTINGS_HANDLER, "SettingsController"),
|
|
7661
|
+
validation: handledBy(SETTINGS_HANDLER, "SettingsController", "apply"),
|
|
6858
7662
|
},
|
|
6859
7663
|
{
|
|
6860
7664
|
field: "largeDocumentWindowDays",
|
|
@@ -6868,7 +7672,7 @@ export const APP_SETTINGS_SURFACE = {
|
|
|
6868
7672
|
// Refused rather than clamped: answering 200 to a 30-day window the
|
|
6869
7673
|
// server does not keep would have the caller plan a month of
|
|
6870
7674
|
// offline work around it (`parseLargeDocumentWindowSetting`).
|
|
6871
|
-
validation: handledBy(SETTINGS_HANDLER, "SettingsController"),
|
|
7675
|
+
validation: handledBy(SETTINGS_HANDLER, "SettingsController", "apply"),
|
|
6872
7676
|
},
|
|
6873
7677
|
],
|
|
6874
7678
|
notExposed: {
|
|
@@ -6986,7 +7790,7 @@ export const APP_SETTINGS_SURFACE = {
|
|
|
6986
7790
|
// per-type secret table, cross-entry URI uniqueness and the
|
|
6987
7791
|
// aggregate caps are all `validateGoogleClientsWrite`
|
|
6988
7792
|
// (`src/services/google-clients.ts`), applied by the handler.
|
|
6989
|
-
validation: handledBy(SETTINGS_HANDLER, "SettingsController"),
|
|
7793
|
+
validation: handledBy(SETTINGS_HANDLER, "SettingsController", "apply"),
|
|
6990
7794
|
},
|
|
6991
7795
|
{
|
|
6992
7796
|
field: "passkeyEnabled",
|
|
@@ -7007,7 +7811,7 @@ export const APP_SETTINGS_SURFACE = {
|
|
|
7007
7811
|
writableOn: UPDATE_ONLY,
|
|
7008
7812
|
// Mirrored into the two retired model fields by the handler so an
|
|
7009
7813
|
// un-migrated reader cannot disagree.
|
|
7010
|
-
validation: handledBy(SETTINGS_HANDLER, "SettingsController"),
|
|
7814
|
+
validation: handledBy(SETTINGS_HANDLER, "SettingsController", "apply"),
|
|
7011
7815
|
},
|
|
7012
7816
|
{
|
|
7013
7817
|
field: "appleSignInEnabled",
|
|
@@ -7037,7 +7841,7 @@ export const APP_SETTINGS_SURFACE = {
|
|
|
7037
7841
|
// `validateRedirectUris`; matching is fail-closed, and a request
|
|
7038
7842
|
// whose target this list does not cover gets a code-only email
|
|
7039
7843
|
// rather than a link (#2884).
|
|
7040
|
-
validation: handledBy(SETTINGS_HANDLER, "SettingsController"),
|
|
7844
|
+
validation: handledBy(SETTINGS_HANDLER, "SettingsController", "apply"),
|
|
7041
7845
|
},
|
|
7042
7846
|
{
|
|
7043
7847
|
field: "passkeyRpConfig",
|
|
@@ -7047,7 +7851,7 @@ export const APP_SETTINGS_SURFACE = {
|
|
|
7047
7851
|
writableOn: UPDATE_ONLY,
|
|
7048
7852
|
// `[auth.passkeys]` — rpId → { name }, shape-checked by
|
|
7049
7853
|
// `SettingsController.validatePasskeyRpConfig`.
|
|
7050
|
-
validation: handledBy(SETTINGS_HANDLER, "SettingsController"),
|
|
7854
|
+
validation: handledBy(SETTINGS_HANDLER, "SettingsController", "apply"),
|
|
7051
7855
|
},
|
|
7052
7856
|
{
|
|
7053
7857
|
field: "passkeyUserVerification",
|
|
@@ -7065,7 +7869,7 @@ export const APP_SETTINGS_SURFACE = {
|
|
|
7065
7869
|
// both halves of every passkey ceremony state, so an unrecognized
|
|
7066
7870
|
// value is a 400 rather than a silent fall back to the default —
|
|
7067
7871
|
// the `directLlmEnabled` rationale, applied to a security policy.
|
|
7068
|
-
validation: handledBy(SETTINGS_HANDLER, "SettingsController"),
|
|
7872
|
+
validation: handledBy(SETTINGS_HANDLER, "SettingsController", "apply"),
|
|
7069
7873
|
},
|
|
7070
7874
|
],
|
|
7071
7875
|
notExposed: {},
|
|
@@ -7535,6 +8339,8 @@ export const GENERATED_CONFIG_MODEL_FIELDS = {
|
|
|
7535
8339
|
"description",
|
|
7536
8340
|
"status",
|
|
7537
8341
|
"activeConfigId",
|
|
8342
|
+
"activeConfigActivatedAt",
|
|
8343
|
+
"activeConfigActivatedBy",
|
|
7538
8344
|
"runAs",
|
|
7539
8345
|
"access",
|
|
7540
8346
|
"inputSchema",
|