primitive-admin 1.1.0-alpha.53 → 1.1.0-alpha.55
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +24 -10
- package/assets/skill/skills/primitive-platform/SKILL.md +134 -0
- package/dist/bin/primitive.js +4 -0
- package/dist/bin/primitive.js.map +1 -1
- package/dist/src/commands/admins.js +22 -8
- package/dist/src/commands/admins.js.map +1 -1
- package/dist/src/commands/analytics.js +71 -1
- package/dist/src/commands/analytics.js.map +1 -1
- package/dist/src/commands/apps.js +16 -14
- package/dist/src/commands/apps.js.map +1 -1
- package/dist/src/commands/blob-buckets.js +37 -2
- package/dist/src/commands/blob-buckets.js.map +1 -1
- package/dist/src/commands/connections.d.ts +2 -0
- package/dist/src/commands/connections.js +95 -0
- package/dist/src/commands/connections.js.map +1 -0
- package/dist/src/commands/databases.js +280 -45
- package/dist/src/commands/databases.js.map +1 -1
- package/dist/src/commands/documents.js +615 -1
- package/dist/src/commands/documents.js.map +1 -1
- package/dist/src/commands/init.js +2 -2
- package/dist/src/commands/init.js.map +1 -1
- package/dist/src/commands/integrations.js +11 -88
- package/dist/src/commands/integrations.js.map +1 -1
- package/dist/src/commands/metadata.js +47 -30
- package/dist/src/commands/metadata.js.map +1 -1
- package/dist/src/commands/prompts.js +14 -5
- package/dist/src/commands/prompts.js.map +1 -1
- package/dist/src/commands/sessions.d.ts +2 -0
- package/dist/src/commands/sessions.js +63 -0
- package/dist/src/commands/sessions.js.map +1 -0
- package/dist/src/commands/settings.d.ts +1 -2
- package/dist/src/commands/settings.js +8 -6
- package/dist/src/commands/settings.js.map +1 -1
- package/dist/src/commands/skill.js +2 -2
- package/dist/src/commands/skill.js.map +1 -1
- package/dist/src/commands/sync-app-settings.d.ts +26 -37
- package/dist/src/commands/sync-app-settings.js +43 -56
- package/dist/src/commands/sync-app-settings.js.map +1 -1
- package/dist/src/commands/sync.d.ts +4 -3
- package/dist/src/commands/sync.js +245 -30
- package/dist/src/commands/sync.js.map +1 -1
- package/dist/src/commands/webhooks.js +71 -23
- package/dist/src/commands/webhooks.js.map +1 -1
- package/dist/src/commands/workflows.js +364 -55
- package/dist/src/commands/workflows.js.map +1 -1
- package/dist/src/lib/api-client.d.ts +294 -13
- package/dist/src/lib/api-client.js +345 -42
- package/dist/src/lib/api-client.js.map +1 -1
- package/dist/src/lib/app-settings-descriptor.d.ts +15 -13
- package/dist/src/lib/app-settings-descriptor.js +25 -23
- package/dist/src/lib/app-settings-descriptor.js.map +1 -1
- package/dist/src/lib/data-input.d.ts +23 -0
- package/dist/src/lib/data-input.js +50 -0
- package/dist/src/lib/data-input.js.map +1 -0
- package/dist/src/lib/generated-allowlist.js +1 -0
- package/dist/src/lib/generated-allowlist.js.map +1 -1
- package/dist/src/lib/generated-workflow-model-fields.d.ts +20 -0
- package/dist/src/lib/generated-workflow-model-fields.js +54 -0
- package/dist/src/lib/generated-workflow-model-fields.js.map +1 -0
- package/dist/src/lib/log-inspection.d.ts +550 -0
- package/dist/src/lib/log-inspection.js +609 -0
- package/dist/src/lib/log-inspection.js.map +1 -0
- package/dist/src/lib/output.d.ts +10 -0
- package/dist/src/lib/output.js +12 -0
- package/dist/src/lib/output.js.map +1 -1
- package/dist/src/lib/record-filter.d.ts +16 -0
- package/dist/src/lib/record-filter.js +53 -0
- package/dist/src/lib/record-filter.js.map +1 -0
- package/dist/src/lib/skill-installer.d.ts +4 -2
- package/dist/src/lib/skill-installer.js +139 -11
- package/dist/src/lib/skill-installer.js.map +1 -1
- package/dist/src/lib/watch.d.ts +121 -0
- package/dist/src/lib/watch.js +169 -0
- package/dist/src/lib/watch.js.map +1 -0
- package/dist/src/lib/workflow-field-descriptor.d.ts +92 -0
- package/dist/src/lib/workflow-field-descriptor.js +163 -0
- package/dist/src/lib/workflow-field-descriptor.js.map +1 -0
- package/dist/src/lib/workflow-fragments.d.ts +23 -0
- package/dist/src/lib/workflow-fragments.js +229 -8
- package/dist/src/lib/workflow-fragments.js.map +1 -1
- package/dist/src/lib/workflow-payload.d.ts +1 -1
- package/dist/src/lib/workflow-payload.js +17 -0
- package/dist/src/lib/workflow-payload.js.map +1 -1
- package/dist/src/lib/workflow-toml-validator.d.ts +57 -1
- package/dist/src/lib/workflow-toml-validator.js +104 -1
- package/dist/src/lib/workflow-toml-validator.js.map +1 -1
- package/dist/src/types/index.d.ts +21 -16
- package/package.json +7 -4
|
@@ -105,6 +105,15 @@ function canonicalJsonStringify(value) {
|
|
|
105
105
|
if (Array.isArray(value)) {
|
|
106
106
|
return "[" + value.map((v) => canonicalJsonStringify(v)).join(",") + "]";
|
|
107
107
|
}
|
|
108
|
+
// Honor `toJSON` before the key walk. A `Date` — or smol-toml's `TomlDate`,
|
|
109
|
+
// what a TOML datetime literal parses to — has no enumerable own keys, so
|
|
110
|
+
// the `Object.keys()` walk below would collapse it to "{}" and every
|
|
111
|
+
// datetime would hash identically (issue #1919). Both implement `toJSON`,
|
|
112
|
+
// returning the ISO string; recursing on it makes date-like values (nested
|
|
113
|
+
// ones too) participate in the hash. Matches JSON.stringify's own semantics.
|
|
114
|
+
if (typeof value.toJSON === "function") {
|
|
115
|
+
return canonicalJsonStringify(value.toJSON());
|
|
116
|
+
}
|
|
108
117
|
const keys = Object.keys(value).sort();
|
|
109
118
|
return ("{" +
|
|
110
119
|
keys
|
|
@@ -249,8 +258,7 @@ export function hashRemoteWorkflowForDiff(workflow, draft, configs) {
|
|
|
249
258
|
* (`cli/src/lib/app-settings-descriptor.ts`), which now owns the [auth] field
|
|
250
259
|
* set that `AUTH_BOOLEAN_KEYS`/`RECOGNIZED_AUTH_KEYS` used to hard-code. The
|
|
251
260
|
* descriptor drives all four sections in one place, so pull, push, and the
|
|
252
|
-
* unrecognized-key warning can never drift apart.
|
|
253
|
-
* secret descriptor entry and is never written.
|
|
261
|
+
* unrecognized-key warning can never drift apart.
|
|
254
262
|
*/
|
|
255
263
|
export function serializeAuthBlock(settings) {
|
|
256
264
|
return serializeSection(settings, "auth");
|
|
@@ -260,7 +268,7 @@ export function serializeAuthBlock(settings) {
|
|
|
260
268
|
* direction). Kept as a focused helper over the shared descriptor; the full
|
|
261
269
|
* push path uses `parseTomlToAppSettings` for every section. Only keys present
|
|
262
270
|
* in the TOML are forwarded, so an omitted key never overwrites server state.
|
|
263
|
-
* Descriptor errors (a
|
|
271
|
+
* Descriptor errors (a type mismatch) are surfaced alongside the
|
|
264
272
|
* unrecognized-key warnings.
|
|
265
273
|
*/
|
|
266
274
|
export function parseAppAuthSettings(auth) {
|
|
@@ -590,6 +598,69 @@ function serializeIntegration(integration) {
|
|
|
590
598
|
};
|
|
591
599
|
return stringifyConfigToml(data);
|
|
592
600
|
}
|
|
601
|
+
/**
|
|
602
|
+
* Map a webhook's stored verification `config` blob to/from the TOML
|
|
603
|
+
* `[verification.<scheme>]` table.
|
|
604
|
+
*
|
|
605
|
+
* The storage shapes differ per scheme and the mapping has to be explicit:
|
|
606
|
+
* `discord` keeps its key material flat at `config.publicKey`, while the
|
|
607
|
+
* JWT-family schemes nest theirs under `config.<scheme>`. Without this the
|
|
608
|
+
* whole `config` blob was dropped on both pull and push, which is why a
|
|
609
|
+
* `discord` webhook could not be configured through the CLI at all.
|
|
610
|
+
*/
|
|
611
|
+
/**
|
|
612
|
+
* Schemes whose stored `config` nests under the scheme name (what the server
|
|
613
|
+
* validates) — the JWT family, including the `plaid` preset (#2109).
|
|
614
|
+
* Everything else — `discord` and any scheme carrying a plain config blob — is
|
|
615
|
+
* stored flat. `webhookConfigToTomlTable` and `tomlTableToWebhookConfig` both
|
|
616
|
+
* read this list, so the two directions cannot disagree: reading the shape off
|
|
617
|
+
* the data on one side and off a different list on the other is what let a
|
|
618
|
+
* pull -> push on a flat-config `stripe`/`custom`/`none` webhook re-shape a
|
|
619
|
+
* config it never meant to change.
|
|
620
|
+
*/
|
|
621
|
+
const NESTED_CONFIG_SCHEMES = new Set(["jwt", "plaid"]);
|
|
622
|
+
export function webhookConfigToTomlTable(scheme, config) {
|
|
623
|
+
const parsed = parseWebhookConfigValue(config);
|
|
624
|
+
if (!parsed)
|
|
625
|
+
return undefined;
|
|
626
|
+
if (NESTED_CONFIG_SCHEMES.has(scheme)) {
|
|
627
|
+
const nested = parsed[scheme];
|
|
628
|
+
if (nested && typeof nested === "object" && !Array.isArray(nested)) {
|
|
629
|
+
return nested;
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
// Flat shape (everything else). An empty object means "nothing to write".
|
|
633
|
+
return Object.keys(parsed).length > 0 ? parsed : undefined;
|
|
634
|
+
}
|
|
635
|
+
export function tomlTableToWebhookConfig(scheme, table) {
|
|
636
|
+
if (!table || typeof table !== "object" || Array.isArray(table)) {
|
|
637
|
+
return undefined;
|
|
638
|
+
}
|
|
639
|
+
if (Object.keys(table).length === 0)
|
|
640
|
+
return undefined;
|
|
641
|
+
// Only the schemes listed nest under their own name; every other scheme
|
|
642
|
+
// stores its config flat. The two directions read the same list, so a
|
|
643
|
+
// pull -> push round-trip cannot re-shape a config it did not change.
|
|
644
|
+
return NESTED_CONFIG_SCHEMES.has(scheme) ? { [scheme]: table } : { ...table };
|
|
645
|
+
}
|
|
646
|
+
function parseWebhookConfigValue(config) {
|
|
647
|
+
if (!config)
|
|
648
|
+
return undefined;
|
|
649
|
+
if (typeof config === "string") {
|
|
650
|
+
try {
|
|
651
|
+
const parsed = JSON.parse(config);
|
|
652
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed)
|
|
653
|
+
? parsed
|
|
654
|
+
: undefined;
|
|
655
|
+
}
|
|
656
|
+
catch {
|
|
657
|
+
return undefined;
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
if (typeof config === "object" && !Array.isArray(config))
|
|
661
|
+
return config;
|
|
662
|
+
return undefined;
|
|
663
|
+
}
|
|
593
664
|
export function serializeWebhook(webhook) {
|
|
594
665
|
const data = {
|
|
595
666
|
webhook: {
|
|
@@ -602,18 +673,19 @@ export function serializeWebhook(webhook) {
|
|
|
602
673
|
toleranceSeconds: webhook.toleranceSeconds,
|
|
603
674
|
deduplicationEnabled: webhook.deduplicationEnabled,
|
|
604
675
|
deduplicationWindowMs: webhook.deduplicationWindowMs,
|
|
676
|
+
// Per-webhook inbound body cap in bytes. Omitted from TOML when unset so
|
|
677
|
+
// the webhook keeps the platform default (5 MiB); a value round-trips.
|
|
678
|
+
maxBodyBytes: webhook.maxBodyBytes ?? undefined,
|
|
605
679
|
secretGracePeriodMs: webhook.secretGracePeriodMs,
|
|
606
680
|
},
|
|
607
681
|
};
|
|
608
|
-
// Re-emit
|
|
609
|
-
// (#1235).
|
|
610
|
-
//
|
|
611
|
-
//
|
|
612
|
-
//
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
// hand-authored spaced reference `{{ secrets.KEY }}` round-trips pull→push.
|
|
616
|
-
/\{\{\s*secrets\.[A-Z][A-Z0-9_]{0,63}\s*\}\}/.test(webhook.signingSecret)) {
|
|
682
|
+
// Re-emit the `{{secrets.KEY}}` reference so a pull→push round-trip is
|
|
683
|
+
// lossless (#1235). `signingSecret` is reference-only (#2254), so the server
|
|
684
|
+
// returns either the reference verbatim or `null` — it never emits secret
|
|
685
|
+
// material, which is why the CLI applies no filtering of its own here. A
|
|
686
|
+
// webhook whose stored value is a legacy literal gets no line at all, and
|
|
687
|
+
// `signingSecretStatus` is what warns the operator about it (see `sync pull`).
|
|
688
|
+
if (typeof webhook.signingSecret === "string" && webhook.signingSecret) {
|
|
617
689
|
data.webhook.signingSecret = webhook.signingSecret;
|
|
618
690
|
}
|
|
619
691
|
// Parse JSON fields if they're strings
|
|
@@ -639,6 +711,15 @@ export function serializeWebhook(webhook) {
|
|
|
639
711
|
}
|
|
640
712
|
catch { }
|
|
641
713
|
}
|
|
714
|
+
// Per-scheme verification settings, e.g.
|
|
715
|
+
// [verification.jwt] header = "..." algorithms = ["ES256"] …
|
|
716
|
+
// [verification.discord] publicKey = "…"
|
|
717
|
+
// `{{secrets.KEY}}` references inside round-trip verbatim, the same rule
|
|
718
|
+
// `signingSecret` follows.
|
|
719
|
+
const verificationTable = webhookConfigToTomlTable(webhook.verificationScheme, webhook.config);
|
|
720
|
+
if (verificationTable) {
|
|
721
|
+
data.verification = { [webhook.verificationScheme]: verificationTable };
|
|
722
|
+
}
|
|
642
723
|
// Remove undefined values
|
|
643
724
|
Object.keys(data.webhook).forEach(k => {
|
|
644
725
|
if (data.webhook[k] === undefined)
|
|
@@ -913,6 +994,16 @@ export function serializeWorkflow(workflow, draft, configs, logger) {
|
|
|
913
994
|
// as a data-loss fallback (issue #1446).
|
|
914
995
|
inputSchema: serializeWorkflowSchemaField(workflow.inputSchema, "inputSchema", workflow.workflowKey, log),
|
|
915
996
|
outputSchema: serializeWorkflowSchemaField(workflow.outputSchema, "outputSchema", workflow.workflowKey, log),
|
|
997
|
+
// #1972 — declarative run-scoped lock (`[workflow.lock]`, #1518 Phase 3).
|
|
998
|
+
// The GET response returns `lock` parsed (an object, null when unset —
|
|
999
|
+
// like `metadataManifest`). Emit the nested `[workflow.lock]` table only
|
|
1000
|
+
// when present so a fresh pull → push round-trips it and an unset lock
|
|
1001
|
+
// doesn't write a noisy empty table (nor a false `sync diff`). Before this
|
|
1002
|
+
// pull dropped the lock entirely, so a pull → push cycle silently cleared
|
|
1003
|
+
// a server-side lock.
|
|
1004
|
+
lock: workflow.lock && typeof workflow.lock === "object"
|
|
1005
|
+
? workflow.lock
|
|
1006
|
+
: undefined,
|
|
916
1007
|
},
|
|
917
1008
|
// Use active config steps, fall back to draft
|
|
918
1009
|
steps,
|
|
@@ -2292,6 +2383,14 @@ Directory Structure:
|
|
|
2292
2383
|
modifiedAt: webhook.modifiedAt || new Date().toISOString(),
|
|
2293
2384
|
contentHash: computeFileHash(filePath),
|
|
2294
2385
|
};
|
|
2386
|
+
// A row still holding a raw secret from before signing secrets became
|
|
2387
|
+
// reference-only (#2254). It is never written to TOML, and every
|
|
2388
|
+
// delivery to it is rejected — pull time is the only place an
|
|
2389
|
+
// operator finds out before the rejections start.
|
|
2390
|
+
if (webhook.signingSecretStatus === "legacy-literal") {
|
|
2391
|
+
warn(` webhooks/${filename}: signingSecret still holds a raw value, so every signed delivery to '${webhook.webhookKey}' is rejected. ` +
|
|
2392
|
+
`Store the value with \`primitive secrets set KEY --value <value>\` and set signingSecret = "{{secrets.KEY}}".`);
|
|
2393
|
+
}
|
|
2295
2394
|
}
|
|
2296
2395
|
info(` Pulled ${webhooks.length} webhook(s)`);
|
|
2297
2396
|
// Pull cron triggers. An older server without the route (or any other
|
|
@@ -3024,7 +3123,8 @@ Directory Structure:
|
|
|
3024
3123
|
// (no more `process.exit` bypass class). The per-loop validators below
|
|
3025
3124
|
// remain as defense-in-depth but are converted to throws too.
|
|
3026
3125
|
const preflightValidationErrors = [];
|
|
3027
|
-
// Validate all workflow TOMLs (
|
|
3126
|
+
// Validate all workflow TOMLs (#685 misnested-header check and the
|
|
3127
|
+
// #1447 system + accessRule lint).
|
|
3028
3128
|
const preflightWorkflowsDir = join(configDir, "workflows");
|
|
3029
3129
|
if (existsSync(preflightWorkflowsDir)) {
|
|
3030
3130
|
for (const file of readdirSync(preflightWorkflowsDir).filter((f) => f.endsWith(".toml"))) {
|
|
@@ -3107,10 +3207,10 @@ Directory Structure:
|
|
|
3107
3207
|
}
|
|
3108
3208
|
}
|
|
3109
3209
|
}
|
|
3110
|
-
// Validate app.toml settings up-front (issue #1033). A
|
|
3111
|
-
//
|
|
3112
|
-
//
|
|
3113
|
-
//
|
|
3210
|
+
// Validate app.toml settings up-front (issue #1033). A mistyped value
|
|
3211
|
+
// is a hard error that must abort BEFORE any mutation — same
|
|
3212
|
+
// fail-before-mutate contract as the vars preflight below. The apply
|
|
3213
|
+
// block re-parses for the payload.
|
|
3114
3214
|
const preflightAppTomlPath = join(configDir, "app.toml");
|
|
3115
3215
|
if (existsSync(preflightAppTomlPath)) {
|
|
3116
3216
|
try {
|
|
@@ -3224,7 +3324,7 @@ Directory Structure:
|
|
|
3224
3324
|
const tomlData = parseTomlFile(appTomlPath);
|
|
3225
3325
|
// Descriptor-driven parse of every section ([app]/[auth]/[cors]/
|
|
3226
3326
|
// [invitations]) — issue #1033. `name` is stripped on cross-app
|
|
3227
|
-
// push. Errors (
|
|
3327
|
+
// push. Errors (type mismatch) already aborted the
|
|
3228
3328
|
// push in the preflight pass above, so only warnings remain here.
|
|
3229
3329
|
const { settings, warnings: appWarnings } = parseTomlToAppSettings(tomlData, { isCrossAppPush });
|
|
3230
3330
|
for (const w of appWarnings) {
|
|
@@ -3648,6 +3748,10 @@ Directory Structure:
|
|
|
3648
3748
|
toleranceSeconds: webhook.toleranceSeconds,
|
|
3649
3749
|
deduplicationEnabled: webhook.deduplicationEnabled,
|
|
3650
3750
|
deduplicationWindowMs: webhook.deduplicationWindowMs,
|
|
3751
|
+
// Body cap is TOML-owned: send `?? null` so removing it from the
|
|
3752
|
+
// TOML clears the override on the server (back to the 5 MiB
|
|
3753
|
+
// default); a declared value is forwarded through the same PATCH.
|
|
3754
|
+
maxBodyBytes: webhook.maxBodyBytes ?? null,
|
|
3651
3755
|
secretGracePeriodMs: webhook.secretGracePeriodMs,
|
|
3652
3756
|
};
|
|
3653
3757
|
// Forward the declared signing secret so a `{{secrets.KEY}}`
|
|
@@ -3668,6 +3772,30 @@ Directory Structure:
|
|
|
3668
3772
|
if (tomlData.metadata) {
|
|
3669
3773
|
payload.metadata = JSON.stringify(tomlData.metadata);
|
|
3670
3774
|
}
|
|
3775
|
+
// Forward the declared `[verification.<scheme>]` table so scheme
|
|
3776
|
+
// configuration reaches the server. Without this the table is
|
|
3777
|
+
// silently dropped and a scheme whose key material lives in
|
|
3778
|
+
// `config` (discord, jwt, plaid) can never be configured from TOML.
|
|
3779
|
+
const declaredVerification = tomlData.verification?.[webhook.verificationScheme];
|
|
3780
|
+
const verificationConfig = tomlTableToWebhookConfig(webhook.verificationScheme, declaredVerification);
|
|
3781
|
+
// Once a file declares a `[verification]` section, that section
|
|
3782
|
+
// owns the stored config — so removing the scheme's table clears
|
|
3783
|
+
// it (`?? null`, the same pattern as `description` above).
|
|
3784
|
+
// Otherwise revoking a compromised key through the supported path
|
|
3785
|
+
// would silently no-op: an omitted `config` is dropped from the
|
|
3786
|
+
// request and the old key keeps verifying deliveries.
|
|
3787
|
+
//
|
|
3788
|
+
// A file with no `[verification]` section at all is treated as "not
|
|
3789
|
+
// managed here" and leaves the stored config alone, so a TOML
|
|
3790
|
+
// written before this section existed still pushes unrelated edits.
|
|
3791
|
+
// To clear the config from such a file, add a bare `[verification]`
|
|
3792
|
+
// table with no scheme sub-table.
|
|
3793
|
+
if (tomlData.verification !== undefined) {
|
|
3794
|
+
payload.config = verificationConfig ?? null;
|
|
3795
|
+
}
|
|
3796
|
+
else if (verificationConfig) {
|
|
3797
|
+
payload.config = verificationConfig;
|
|
3798
|
+
}
|
|
3671
3799
|
if (existingId) {
|
|
3672
3800
|
changes.push({ type: "webhook", action: "update", key });
|
|
3673
3801
|
if (!options.dryRun) {
|
|
@@ -4445,10 +4573,12 @@ Directory Structure:
|
|
|
4445
4573
|
info(` Skipped workflows/${file} — platform-owned workflow "${platformOwnedKey}" (managed by the platform)`);
|
|
4446
4574
|
continue;
|
|
4447
4575
|
}
|
|
4448
|
-
//
|
|
4449
|
-
//
|
|
4450
|
-
//
|
|
4451
|
-
// place to catch the footgun
|
|
4576
|
+
// Reject misnested headers (#685, e.g. [steps.<id>.request]) and
|
|
4577
|
+
// the #1447 runAs="system" + accessRule dead-config combo before
|
|
4578
|
+
// pushing. The runtime silently ignores fields outside the
|
|
4579
|
+
// allowlist, so this is the only place to catch the footgun, and
|
|
4580
|
+
// the server rejects a system accessRule (#1258) — flagging it
|
|
4581
|
+
// here keeps the push all-or-nothing. Validation happens BEFORE the
|
|
4452
4582
|
// skip-if-unchanged check so a previously-pushed-but-broken
|
|
4453
4583
|
// file gets a clear diagnostic on every push attempt.
|
|
4454
4584
|
// Issue #976: validation is now front-loaded before any apply
|
|
@@ -4936,6 +5066,28 @@ Directory Structure:
|
|
|
4936
5066
|
const validateExpectedModifiedAt = options.force
|
|
4937
5067
|
? undefined
|
|
4938
5068
|
: existingEntry?.modifiedAt;
|
|
5069
|
+
// Issue #1336 (codex round-1 P1): the final post-push rule content
|
|
5070
|
+
// of every op this push lands. When the push ALSO changes the
|
|
5071
|
+
// manifest, the server re-lints child ops against the new manifest;
|
|
5072
|
+
// forwarding each op's proposed `access`/`params` lets that re-lint
|
|
5073
|
+
// evaluate the op's post-push form instead of the stale stored row,
|
|
5074
|
+
// so an atomic manifest-rename + op-rewrite push is not falsely
|
|
5075
|
+
// rejected. A field is sent as value-or-null (config-as-code: the
|
|
5076
|
+
// TOML op IS the final state).
|
|
5077
|
+
const pendingOpUpdates = operations.map((op) => ({
|
|
5078
|
+
name: op.name,
|
|
5079
|
+
access: typeof op.access === "string" ? op.access : null,
|
|
5080
|
+
params: op.params ?? null,
|
|
5081
|
+
}));
|
|
5082
|
+
// Issue #1336 (codex round-1 P1/P2): the declared-access manifest
|
|
5083
|
+
// this push proposes (the parsed object, or null when the type
|
|
5084
|
+
// declares none / is clearing it). Forwarded as
|
|
5085
|
+
// `metadataManifestOverride` on the op dry-runs so op-access lints
|
|
5086
|
+
// run against the manifest the same push is landing, not the stale
|
|
5087
|
+
// stored one (existing type: a newly-declared key is honored; fresh
|
|
5088
|
+
// type: an undeclared ref is caught instead of skipped).
|
|
5089
|
+
const proposedManifest = typeConfig.metadataManifest ??
|
|
5090
|
+
null;
|
|
4939
5091
|
try {
|
|
4940
5092
|
// 1. Schema-edit gate (type-config PATCH dry-run) — only when the
|
|
4941
5093
|
// type already exists on the server and we'd actually PATCH a
|
|
@@ -4948,13 +5100,53 @@ Directory Structure:
|
|
|
4948
5100
|
// type-config dry-run at all.
|
|
4949
5101
|
if (existingEntry && typeLevelChange) {
|
|
4950
5102
|
const validateUpdateData = computeTypeUpdateData();
|
|
4951
|
-
|
|
4952
|
-
|
|
4953
|
-
|
|
4954
|
-
|
|
4955
|
-
|
|
4956
|
-
|
|
4957
|
-
|
|
5103
|
+
const baseValidate = pendingOpDeletes.length > 0
|
|
5104
|
+
? { ...validateUpdateData, pendingOpDeletes, finalOpNames }
|
|
5105
|
+
: validateUpdateData;
|
|
5106
|
+
await client.updateDatabaseTypeConfig(resolvedAppId, dbType,
|
|
5107
|
+
// Issue #1336 (P1): forward the pending op rewrites so the
|
|
5108
|
+
// server's child-op re-lint uses each op's post-push content.
|
|
5109
|
+
{ ...baseValidate, pendingOpUpdates }, validateExpectedModifiedAt, {
|
|
5110
|
+
dryRun: true,
|
|
5111
|
+
acceptWarnings: !!options.acceptWarnings,
|
|
5112
|
+
});
|
|
5113
|
+
}
|
|
5114
|
+
else if (!existingEntry) {
|
|
5115
|
+
// Fresh type: run the config CREATE dry-run (issue #1336, codex
|
|
5116
|
+
// round-1 P2). Before this, a brand-new type's inline config
|
|
5117
|
+
// rules (`defaultAccess`, `metadataAccess`, autoPopulatedFields,
|
|
5118
|
+
// triggers) and its manifest were never dry-run — a malformed
|
|
5119
|
+
// rule reported a clean `sync push --dry-run` then failed only on
|
|
5120
|
+
// the real POST. Mirror the apply-path create payload below.
|
|
5121
|
+
const createDryRunData = { databaseType: dbType };
|
|
5122
|
+
if (typeConfig.ruleSetId)
|
|
5123
|
+
createDryRunData.ruleSetId = typeConfig.ruleSetId;
|
|
5124
|
+
if (typeConfig.triggers)
|
|
5125
|
+
createDryRunData.triggers = typeConfig.triggers;
|
|
5126
|
+
if (typeConfig.metadataAccess)
|
|
5127
|
+
createDryRunData.metadataAccess = typeConfig.metadataAccess;
|
|
5128
|
+
if (typeConfig.defaultAccess)
|
|
5129
|
+
createDryRunData.defaultAccess = typeConfig.defaultAccess;
|
|
5130
|
+
if (typeConfig.autoPopulatedFields)
|
|
5131
|
+
createDryRunData.autoPopulatedFields =
|
|
5132
|
+
typeConfig.autoPopulatedFields;
|
|
5133
|
+
if (typeConfig.timestamps)
|
|
5134
|
+
createDryRunData.timestamps = typeConfig.timestamps;
|
|
5135
|
+
if (typeConfig.schema)
|
|
5136
|
+
createDryRunData.schema = typeConfig.schema;
|
|
5137
|
+
if (typeConfig.metadataManifest)
|
|
5138
|
+
createDryRunData.metadataManifest = typeConfig.metadataManifest;
|
|
5139
|
+
try {
|
|
5140
|
+
await client.createDatabaseTypeConfig(resolvedAppId, createDryRunData, { dryRun: true });
|
|
5141
|
+
}
|
|
5142
|
+
catch (createErr) {
|
|
5143
|
+
// A 409 means the type already exists server-side (local sync
|
|
5144
|
+
// state was lost); the fresh-type inline-rule validation does
|
|
5145
|
+
// not apply and the real push reconciles separately. Any other
|
|
5146
|
+
// error is a genuine create-time validation failure — re-throw
|
|
5147
|
+
// so the catch below reports it.
|
|
5148
|
+
if (createErr?.statusCode !== 409)
|
|
5149
|
+
throw createErr;
|
|
4958
5150
|
}
|
|
4959
5151
|
}
|
|
4960
5152
|
// 2. Op-edit gate (op create/update dry-run) for every op in the
|
|
@@ -5012,7 +5204,14 @@ Directory Structure:
|
|
|
5012
5204
|
access: op.access,
|
|
5013
5205
|
definition: op.definition,
|
|
5014
5206
|
params: op.params,
|
|
5015
|
-
}, options.force ? undefined : existingOp.modifiedAt, {
|
|
5207
|
+
}, options.force ? undefined : existingOp.modifiedAt, {
|
|
5208
|
+
dryRun: true,
|
|
5209
|
+
schemaOverride: proposedSchema,
|
|
5210
|
+
// Issue #1336 (P1/P2): gate the op-access lint against
|
|
5211
|
+
// the manifest THIS push is landing, not the stale
|
|
5212
|
+
// stored one.
|
|
5213
|
+
metadataManifestOverride: proposedManifest,
|
|
5214
|
+
});
|
|
5016
5215
|
}
|
|
5017
5216
|
else {
|
|
5018
5217
|
await client.createDatabaseTypeOperation(resolvedAppId, dbType, {
|
|
@@ -5025,6 +5224,10 @@ Directory Structure:
|
|
|
5025
5224
|
}, {
|
|
5026
5225
|
dryRun: true,
|
|
5027
5226
|
schemaOverride: proposedSchema,
|
|
5227
|
+
// Issue #1336 (P1/P2): as above — a fresh-type op dry-run
|
|
5228
|
+
// now lints against the proposed manifest instead of
|
|
5229
|
+
// skipping the lint entirely.
|
|
5230
|
+
metadataManifestOverride: proposedManifest,
|
|
5028
5231
|
// Issue #915 (defect a, follow-up): for a fresh type the
|
|
5029
5232
|
// server has no stored config to read `defaultAccess`
|
|
5030
5233
|
// from, so an op that omits `access` to inherit the
|
|
@@ -5209,6 +5412,18 @@ Directory Structure:
|
|
|
5209
5412
|
// exclusion (see derivation above).
|
|
5210
5413
|
updateData.finalOpNames = finalOpNames;
|
|
5211
5414
|
}
|
|
5415
|
+
// Issue #1336 (codex round-2 P1): forward the pending op
|
|
5416
|
+
// rewrites on the REAL mutating PATCH too, mirroring the
|
|
5417
|
+
// dry-run validate pass above. Without this, when the same
|
|
5418
|
+
// push replaces a manifest key AND rewrites an op that
|
|
5419
|
+
// references the old key, the server's child-op re-lint runs
|
|
5420
|
+
// against the stale stored op → INVALID_RULE → the config
|
|
5421
|
+
// PATCH aborts before the op PATCH lands. Sending the final
|
|
5422
|
+
// op forms lets the re-lint evaluate each op's post-push
|
|
5423
|
+
// content, so the atomic manifest-rename + op-rewrite push
|
|
5424
|
+
// converges. Sent unconditionally (as the dry-run does); a
|
|
5425
|
+
// field absent from a claim means "not changed" server-side.
|
|
5426
|
+
updateData.pendingOpUpdates = pendingOpUpdates;
|
|
5212
5427
|
if (typeLevelChange) {
|
|
5213
5428
|
changes.push({ type: "database-type", action: "update", key: dbType });
|
|
5214
5429
|
const updated = await client.updateDatabaseTypeConfig(resolvedAppId, dbType, updateData, expectedModifiedAt, {
|