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.
Files changed (88) hide show
  1. package/README.md +24 -10
  2. package/assets/skill/skills/primitive-platform/SKILL.md +134 -0
  3. package/dist/bin/primitive.js +4 -0
  4. package/dist/bin/primitive.js.map +1 -1
  5. package/dist/src/commands/admins.js +22 -8
  6. package/dist/src/commands/admins.js.map +1 -1
  7. package/dist/src/commands/analytics.js +71 -1
  8. package/dist/src/commands/analytics.js.map +1 -1
  9. package/dist/src/commands/apps.js +16 -14
  10. package/dist/src/commands/apps.js.map +1 -1
  11. package/dist/src/commands/blob-buckets.js +37 -2
  12. package/dist/src/commands/blob-buckets.js.map +1 -1
  13. package/dist/src/commands/connections.d.ts +2 -0
  14. package/dist/src/commands/connections.js +95 -0
  15. package/dist/src/commands/connections.js.map +1 -0
  16. package/dist/src/commands/databases.js +280 -45
  17. package/dist/src/commands/databases.js.map +1 -1
  18. package/dist/src/commands/documents.js +615 -1
  19. package/dist/src/commands/documents.js.map +1 -1
  20. package/dist/src/commands/init.js +2 -2
  21. package/dist/src/commands/init.js.map +1 -1
  22. package/dist/src/commands/integrations.js +11 -88
  23. package/dist/src/commands/integrations.js.map +1 -1
  24. package/dist/src/commands/metadata.js +47 -30
  25. package/dist/src/commands/metadata.js.map +1 -1
  26. package/dist/src/commands/prompts.js +14 -5
  27. package/dist/src/commands/prompts.js.map +1 -1
  28. package/dist/src/commands/sessions.d.ts +2 -0
  29. package/dist/src/commands/sessions.js +63 -0
  30. package/dist/src/commands/sessions.js.map +1 -0
  31. package/dist/src/commands/settings.d.ts +1 -2
  32. package/dist/src/commands/settings.js +8 -6
  33. package/dist/src/commands/settings.js.map +1 -1
  34. package/dist/src/commands/skill.js +2 -2
  35. package/dist/src/commands/skill.js.map +1 -1
  36. package/dist/src/commands/sync-app-settings.d.ts +26 -37
  37. package/dist/src/commands/sync-app-settings.js +43 -56
  38. package/dist/src/commands/sync-app-settings.js.map +1 -1
  39. package/dist/src/commands/sync.d.ts +4 -3
  40. package/dist/src/commands/sync.js +245 -30
  41. package/dist/src/commands/sync.js.map +1 -1
  42. package/dist/src/commands/webhooks.js +71 -23
  43. package/dist/src/commands/webhooks.js.map +1 -1
  44. package/dist/src/commands/workflows.js +364 -55
  45. package/dist/src/commands/workflows.js.map +1 -1
  46. package/dist/src/lib/api-client.d.ts +294 -13
  47. package/dist/src/lib/api-client.js +345 -42
  48. package/dist/src/lib/api-client.js.map +1 -1
  49. package/dist/src/lib/app-settings-descriptor.d.ts +15 -13
  50. package/dist/src/lib/app-settings-descriptor.js +25 -23
  51. package/dist/src/lib/app-settings-descriptor.js.map +1 -1
  52. package/dist/src/lib/data-input.d.ts +23 -0
  53. package/dist/src/lib/data-input.js +50 -0
  54. package/dist/src/lib/data-input.js.map +1 -0
  55. package/dist/src/lib/generated-allowlist.js +1 -0
  56. package/dist/src/lib/generated-allowlist.js.map +1 -1
  57. package/dist/src/lib/generated-workflow-model-fields.d.ts +20 -0
  58. package/dist/src/lib/generated-workflow-model-fields.js +54 -0
  59. package/dist/src/lib/generated-workflow-model-fields.js.map +1 -0
  60. package/dist/src/lib/log-inspection.d.ts +550 -0
  61. package/dist/src/lib/log-inspection.js +609 -0
  62. package/dist/src/lib/log-inspection.js.map +1 -0
  63. package/dist/src/lib/output.d.ts +10 -0
  64. package/dist/src/lib/output.js +12 -0
  65. package/dist/src/lib/output.js.map +1 -1
  66. package/dist/src/lib/record-filter.d.ts +16 -0
  67. package/dist/src/lib/record-filter.js +53 -0
  68. package/dist/src/lib/record-filter.js.map +1 -0
  69. package/dist/src/lib/skill-installer.d.ts +4 -2
  70. package/dist/src/lib/skill-installer.js +139 -11
  71. package/dist/src/lib/skill-installer.js.map +1 -1
  72. package/dist/src/lib/watch.d.ts +121 -0
  73. package/dist/src/lib/watch.js +169 -0
  74. package/dist/src/lib/watch.js.map +1 -0
  75. package/dist/src/lib/workflow-field-descriptor.d.ts +92 -0
  76. package/dist/src/lib/workflow-field-descriptor.js +163 -0
  77. package/dist/src/lib/workflow-field-descriptor.js.map +1 -0
  78. package/dist/src/lib/workflow-fragments.d.ts +23 -0
  79. package/dist/src/lib/workflow-fragments.js +229 -8
  80. package/dist/src/lib/workflow-fragments.js.map +1 -1
  81. package/dist/src/lib/workflow-payload.d.ts +1 -1
  82. package/dist/src/lib/workflow-payload.js +17 -0
  83. package/dist/src/lib/workflow-payload.js.map +1 -1
  84. package/dist/src/lib/workflow-toml-validator.d.ts +57 -1
  85. package/dist/src/lib/workflow-toml-validator.js +104 -1
  86. package/dist/src/lib/workflow-toml-validator.js.map +1 -1
  87. package/dist/src/types/index.d.ts +21 -16
  88. 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. `googleClientSecret` is a
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 secret key, a type mismatch) are surfaced alongside the
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 a `{{secrets.KEY}}` reference so a pull→push round-trip is lossless
609
- // (#1235). The server only returns `signingSecret` when it is such a
610
- // reference (a real secret is returned masked, never in plaintext); guard on
611
- // the template shape here too so a masked or raw value can never be written
612
- // to TOML.
613
- if (typeof webhook.signingSecret === "string" &&
614
- // Whitespace-tolerant (#1468) to match the server's shared resolver, so a
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 (issue #685 misnested-header check).
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 hand-added
3111
- // `googleClientSecret` key or a mistyped value is a hard error that must
3112
- // abort BEFORE any mutation — same fail-before-mutate contract as the
3113
- // vars preflight below. The apply block re-parses for the payload.
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 (secret key / type mismatch) already aborted the
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
- // Issue #685: reject misnested headers (e.g.
4449
- // [steps.<id>.request]) before pushing. The runtime silently
4450
- // ignores fields outside the allowlist, so this is the only
4451
- // place to catch the footgun. Validation happens BEFORE the
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
- await client.updateDatabaseTypeConfig(resolvedAppId, dbType, pendingOpDeletes.length > 0
4953
- ? { ...validateUpdateData, pendingOpDeletes, finalOpNames }
4954
- : validateUpdateData, validateExpectedModifiedAt, {
4955
- dryRun: true,
4956
- acceptWarnings: !!options.acceptWarnings,
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, { dryRun: true, schemaOverride: proposedSchema });
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, {