@orchestraworks/cli 0.5.8 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (91) hide show
  1. package/README.md +1 -1
  2. package/dist/cli/api/approval.js +3 -3
  3. package/dist/cli/commands/outpost/local.js +3 -3
  4. package/dist/cli/commands/outpost/machine-names.d.ts +17 -0
  5. package/dist/cli/commands/outpost/machine-names.js +22 -0
  6. package/dist/cli/commands/outpost/up.js +7 -5
  7. package/dist/cli/commands/worker/enroll.d.ts +4 -3
  8. package/dist/cli/commands/worker/enroll.js +4 -3
  9. package/dist/cli/core/auth.d.ts +11 -1
  10. package/dist/cli/core/auth.js +21 -4
  11. package/dist/cli/core/session-body.d.ts +1 -1
  12. package/dist/cli/core/session-body.js +1 -1
  13. package/dist/cli/firstRun.js +2 -2
  14. package/dist/cli/main.js +4 -3
  15. package/dist/constructs/environment.d.ts +24 -11
  16. package/dist/constructs/environment.js +76 -12
  17. package/dist/constructs/secret.d.ts +17 -7
  18. package/dist/constructs/secret.js +24 -16
  19. package/dist/core/labels.d.ts +32 -6
  20. package/dist/core/labels.js +70 -8
  21. package/dist/engine/diff.js +38 -11
  22. package/dist/engine/gateway-key-reconcile.d.ts +1 -2
  23. package/dist/engine/gateway-key-reconcile.js +19 -15
  24. package/dist/engine/pipeline-reconcile.js +2 -2
  25. package/dist/engine/pipeline-workflow-reconcile.js +15 -9
  26. package/dist/engine/reconcile.js +48 -15
  27. package/dist/engine/refusals.d.ts +0 -5
  28. package/dist/engine/refusals.js +1 -5
  29. package/dist/engine/secrets-apply.d.ts +10 -14
  30. package/dist/engine/secrets-apply.js +12 -17
  31. package/dist/engine/workflow-reconcile.d.ts +1 -1
  32. package/dist/generated/cli-commands.json +170 -34
  33. package/dist/generated/providers.d.ts +40 -0
  34. package/dist/generated/providers.js +30 -0
  35. package/docs/REFERENCE.md +9 -7
  36. package/node_modules/@orchestraworks/client/dist/esm/models/ApiErrorDetail.d.ts +1 -1
  37. package/node_modules/@orchestraworks/client/dist/esm/models/CreateEnvironmentRequest.d.ts +7 -0
  38. package/node_modules/@orchestraworks/client/dist/esm/models/CreateEnvironmentRequest.js +3 -0
  39. package/node_modules/@orchestraworks/client/dist/esm/models/CreateSecretRequest.d.ts +4 -3
  40. package/node_modules/@orchestraworks/client/dist/esm/models/Environment.d.ts +7 -0
  41. package/node_modules/@orchestraworks/client/dist/esm/models/Environment.js +3 -0
  42. package/node_modules/@orchestraworks/client/dist/esm/models/EnvironmentCredential.d.ts +38 -0
  43. package/node_modules/@orchestraworks/client/dist/esm/models/EnvironmentCredential.js +45 -0
  44. package/node_modules/@orchestraworks/client/dist/esm/models/EnvironmentProvisionSpec.d.ts +1 -1
  45. package/node_modules/@orchestraworks/client/dist/esm/models/EnvironmentProvisionSpecRequest.d.ts +1 -1
  46. package/node_modules/@orchestraworks/client/dist/esm/models/EnvironmentSnapshot.d.ts +1 -1
  47. package/node_modules/@orchestraworks/client/dist/esm/models/InjectionRecipe.d.ts +6 -0
  48. package/node_modules/@orchestraworks/client/dist/esm/models/InjectionRecipe.js +2 -0
  49. package/node_modules/@orchestraworks/client/dist/esm/models/OAuthTokenRequest.d.ts +1 -1
  50. package/node_modules/@orchestraworks/client/dist/esm/models/Secret.d.ts +1 -7
  51. package/node_modules/@orchestraworks/client/dist/esm/models/Secret.js +0 -4
  52. package/node_modules/@orchestraworks/client/dist/esm/models/UpdateEnvironmentRequest.d.ts +7 -0
  53. package/node_modules/@orchestraworks/client/dist/esm/models/UpdateEnvironmentRequest.js +3 -0
  54. package/node_modules/@orchestraworks/client/dist/esm/models/UpdateSecretRequest.d.ts +4 -3
  55. package/node_modules/@orchestraworks/client/dist/esm/models/index.d.ts +1 -0
  56. package/node_modules/@orchestraworks/client/dist/esm/models/index.js +1 -0
  57. package/node_modules/@orchestraworks/client/dist/models/ApiErrorDetail.d.ts +1 -1
  58. package/node_modules/@orchestraworks/client/dist/models/CreateEnvironmentRequest.d.ts +7 -0
  59. package/node_modules/@orchestraworks/client/dist/models/CreateEnvironmentRequest.js +3 -0
  60. package/node_modules/@orchestraworks/client/dist/models/CreateSecretRequest.d.ts +4 -3
  61. package/node_modules/@orchestraworks/client/dist/models/Environment.d.ts +7 -0
  62. package/node_modules/@orchestraworks/client/dist/models/Environment.js +3 -0
  63. package/node_modules/@orchestraworks/client/dist/models/EnvironmentCredential.d.ts +38 -0
  64. package/node_modules/@orchestraworks/client/dist/models/EnvironmentCredential.js +52 -0
  65. package/node_modules/@orchestraworks/client/dist/models/EnvironmentProvisionSpec.d.ts +1 -1
  66. package/node_modules/@orchestraworks/client/dist/models/EnvironmentProvisionSpecRequest.d.ts +1 -1
  67. package/node_modules/@orchestraworks/client/dist/models/EnvironmentSnapshot.d.ts +1 -1
  68. package/node_modules/@orchestraworks/client/dist/models/InjectionRecipe.d.ts +6 -0
  69. package/node_modules/@orchestraworks/client/dist/models/InjectionRecipe.js +2 -0
  70. package/node_modules/@orchestraworks/client/dist/models/OAuthTokenRequest.d.ts +1 -1
  71. package/node_modules/@orchestraworks/client/dist/models/Secret.d.ts +1 -7
  72. package/node_modules/@orchestraworks/client/dist/models/Secret.js +0 -4
  73. package/node_modules/@orchestraworks/client/dist/models/UpdateEnvironmentRequest.d.ts +7 -0
  74. package/node_modules/@orchestraworks/client/dist/models/UpdateEnvironmentRequest.js +3 -0
  75. package/node_modules/@orchestraworks/client/dist/models/UpdateSecretRequest.d.ts +4 -3
  76. package/node_modules/@orchestraworks/client/dist/models/index.d.ts +1 -0
  77. package/node_modules/@orchestraworks/client/dist/models/index.js +1 -0
  78. package/node_modules/@orchestraworks/client/package.json +1 -1
  79. package/package.json +4 -4
  80. package/skills/orchestra/SKILL.md +30 -14
  81. package/skills/orchestra-fdk/references/authoring.md +26 -20
  82. package/skills/orchestra-fdk/references/environment.md +49 -12
  83. package/skills/orchestra-fdk/references/integrations/code-repo.md +26 -5
  84. package/skills/orchestra-fdk/references/phases/1-discover.md +1 -1
  85. package/skills/orchestra-fdk/references/phases/2-plan.md +1 -1
  86. package/skills/orchestra-fdk/references/troubleshooting.md +3 -2
  87. package/templates/bug-fix/main.ts +7 -4
  88. package/templates/engineering-team/lib/environment.ts +6 -3
  89. package/templates/engineering-team/package.json +1 -1
  90. package/templates/todo-factory/main.ts +7 -4
  91. package/templates/todo-factory/package.json +1 -1
@@ -77,6 +77,7 @@ export function cloneInjectionRecipe(recipe) {
77
77
  return {
78
78
  headers: (recipe.headers ?? []).map((h) => ({ name: h.name, value: h.value })),
79
79
  removeHeaders: [...recipe.removeHeaders],
80
+ ...(recipe.hosts !== undefined ? { hosts: [...recipe.hosts] } : {}),
80
81
  };
81
82
  }
82
83
  /**
@@ -319,16 +320,25 @@ function isIpv6Literal(host) {
319
320
  /**
320
321
  * The client-side mirror of the platform's write-time `hosts` checks, so a bad list fails at synth: each
321
322
  * entry is an exact host, `*.` and a suffix, or `*`, and a secret that covers a host needs a recipe header to
322
- * be presented by. The platform-origin and object-store refusals need the deployment's own addresses, so they
323
+ * be presented by. The deprecated top-level `hosts` is moved into the recipe, and refused when the recipe
324
+ * declares a different list, an empty one included. The platform-origin and object-store refusals need the deployment's own addresses, so they
323
325
  * stay the platform's `400`.
324
326
  */
325
- function normalizeSecretHosts(hosts, injection, label) {
326
- if (hosts === undefined)
327
- return [];
328
- normalizeCredentialHosts(hosts, label);
327
+ function normalizeSecretHosts(recipeHosts, deprecatedHosts, injection, label) {
328
+ if (recipeHosts !== undefined)
329
+ normalizeCredentialHosts(recipeHosts, `${label}: injection`);
330
+ if (deprecatedHosts !== undefined)
331
+ normalizeCredentialHosts(deprecatedHosts, label);
332
+ if (recipeHosts !== undefined &&
333
+ deprecatedHosts !== undefined &&
334
+ (recipeHosts.length !== deprecatedHosts.length || recipeHosts.some((host, index) => host !== deprecatedHosts[index]))) {
335
+ throw new Error(`${label}: 'hosts' and 'injection.hosts' name different hosts; declare 'injection.hosts' alone.`);
336
+ }
337
+ const hosts = recipeHosts ?? deprecatedHosts ?? [];
329
338
  if (hosts.length > 0 && (injection === undefined || (injection.headers ?? []).length === 0)) {
330
- throw new Error(`${label}: a secret with 'hosts' needs an 'injection' recipe with at least one header, which is how it ` +
331
- `is presented, such as injection: { headers: [{ name: "Authorization", value: "Bearer \${secret}" }] }.`);
339
+ throw new Error(`${label}: a secret with hosts needs an 'injection' recipe with at least one header, which is how it ` +
340
+ `is presented, such as injection: { headers: [{ name: "Authorization", value: "Bearer \${secret}" }], ` +
341
+ `hosts: ["api.example.com"] }.`);
332
342
  }
333
343
  return [...hosts];
334
344
  }
@@ -337,7 +347,6 @@ export class Secret extends Construct {
337
347
  secretId;
338
348
  #valueFrom;
339
349
  #injection;
340
- #hosts;
341
350
  explicitSlug;
342
351
  displayName;
343
352
  description;
@@ -347,8 +356,9 @@ export class Secret extends Construct {
347
356
  validateExplicitSlug(this, props.slug, "secret");
348
357
  const label = `secret '${this.node.path.join("/")}'`;
349
358
  this.#valueFrom = props.value._toValueFrom();
350
- this.#injection = props.injection === undefined ? undefined : normalizeInjectionRecipe(props.injection, label);
351
- this.#hosts = normalizeSecretHosts(props.hosts, this.#injection, label);
359
+ const recipe = props.injection === undefined ? undefined : normalizeInjectionRecipe(props.injection, label);
360
+ const hosts = normalizeSecretHosts(props.injection?.hosts, props.hosts, recipe, label);
361
+ this.#injection = recipe === undefined ? undefined : { ...recipe, hosts };
352
362
  this.explicitSlug = props.slug;
353
363
  this.displayName = props.name ?? id;
354
364
  this.description = props.description ?? "";
@@ -368,7 +378,7 @@ export class Secret extends Construct {
368
378
  }
369
379
  /** @internal — a fresh copy of the declared hosts, for synth checks that read a declared secret. */
370
380
  _hosts() {
371
- return [...this.#hosts];
381
+ return [...(this.#injection?.hosts ?? [])];
372
382
  }
373
383
  /** @internal — whether the declared recipe has a header to present this secret by. */
374
384
  _presentsHeader() {
@@ -385,16 +395,14 @@ export class Secret extends Construct {
385
395
  const slug = this.slug();
386
396
  // `description` is ALWAYS emitted, empty when unset, so it is never omit-masked; the only Secret PATCH
387
397
  // rides a value overwrite that sends name+description together, so a clear travels with that write.
388
- // `hosts` is ALWAYS emitted, `[]` when undeclared, so apply sends it and a row at `[]` or with other hosts
389
- // converges on the declaration.
390
- // `injection` is emitted only when declared, keeping a recipe-less secret's payload byte-identical, and
391
- // is DEEP-COPIED because the stored recipe is shared across synths: emitting it by reference would let a
398
+ // `injection` is emitted only when declared, its `hosts` always, `[]` when undeclared, so a live recipe with
399
+ // other hosts converges on the declaration and a dropped recipe is cleared, leaving the secret stored only.
400
+ // It is DEEP-COPIED because the stored recipe is shared across synths: emitting it by reference would let a
392
401
  // caller mutating one returned plan corrupt the validated recipe for every later synth.
393
402
  const payload = {
394
403
  name: this.displayName,
395
404
  slug,
396
405
  description: this.description,
397
- hosts: [...this.#hosts],
398
406
  ...(this.#injection !== undefined ? { injection: cloneInjectionRecipe(this.#injection) } : {}),
399
407
  ...(this.team !== undefined ? { teamId: this.team.teamId } : {}),
400
408
  };
@@ -2,18 +2,23 @@
2
2
  * Reserving the whole prefix rather than one literal key is what makes the ownership marker unforgeable and
3
3
  * leaves room for the other markers `apply` writes without a second rule.
4
4
  */
5
- export declare const LABEL_KEY_PREFIX = "sfdk.";
5
+ export declare const LABEL_KEY_PREFIX = "orchestra.";
6
+ /**
7
+ * The prefix `apply` wrote before the rename. A factory applied then carries its markers under it until its next
8
+ * apply moves them, so every marker is read under both prefixes, the new key winning, and both stay reserved.
9
+ */
10
+ export declare const OLD_LABEL_KEY_PREFIX = "sfdk.";
6
11
  /**
7
12
  * Stamped on every resource `apply` creates or adopts among the kinds whose API request exposes `labels`.
8
13
  * Its value is the root factory name, which is always present — even for a factory that declares no team — so a CLI
9
14
  * write against a managed resource can name the owning factory verbatim.
10
15
  */
11
- export declare const MANAGED_BY_LABEL_KEY = "sfdk.managedBy";
16
+ export declare const MANAGED_BY_LABEL_KEY = "orchestra.managedBy";
12
17
  export declare function rejectTemplateBraces(map: Record<string, string> | undefined, where: string): void;
13
18
  /**
14
19
  * A synth-time rejection rather than an apply-time overwrite on purpose: a declared key that could shadow
15
- * `sfdk.managedBy` would make the ownership marker forgeable and make a re-apply fight itself over the same
16
- * key. Refusing at the authoring boundary leaves the marker exactly one writer.
20
+ * the managed-by marker, under either prefix, would make the ownership marker forgeable and make a re-apply
21
+ * fight itself over the same key. Refusing at the authoring boundary leaves the marker exactly one writer.
17
22
  */
18
23
  export declare function rejectReservedLabelKeys(map: Record<string, string> | undefined, where: string): void;
19
24
  /** Constructs call this rather than the two halves, so a new rule lands everywhere at once. */
@@ -28,12 +33,33 @@ export declare function stampManagedBy(labels: Record<string, string> | undefine
28
33
  * an explicit `null`. This record of the keys `apply` last wrote from a declaration is how a later apply
29
34
  * tells a key the declaration dropped, which it removes, from a key someone else set, which it leaves alone.
30
35
  */
31
- export declare const DECLARED_LABELS_KEY = "sfdk.declaredLabels";
36
+ export declare const DECLARED_LABELS_KEY = "orchestra.declaredLabels";
37
+ /**
38
+ * Set on a gateway key's paired Secret while a mint is in flight, so a mint whose token write never landed is
39
+ * seen and refused by name rather than read as a usable Secret.
40
+ */
41
+ export declare const PENDING_GATEWAY_KEY_TOKEN_LABEL_KEY = "orchestra.pendingGatewayKeyToken";
42
+ /** The key a marker had under the old prefix. */
43
+ export declare function oldMarkerKey(key: string): string;
44
+ /** A marker's value: under its key, else under the old prefix's, else `undefined`. */
45
+ export declare function readMarker(labels: Record<string, unknown> | null | undefined, key: string): string | undefined;
46
+ /** A merge patch removing each marker these labels carry under the old prefix. */
47
+ export declare function oldMarkersCleared(labels: Record<string, unknown> | null | undefined): Record<string, null>;
48
+ /**
49
+ * The merge patch that moves each marker found under the old prefix, for a resource whose labels apply does not
50
+ * otherwise reconcile: the old key sent as `null`, and the new key given the old value unless it is already
51
+ * live. `undefined` when no marker is under the old prefix.
52
+ */
53
+ export declare function oldMarkersMoved(labels: Record<string, unknown> | null | undefined): Record<string, string | null> | undefined;
54
+ /** The factory whose apply stamped these labels, read under either prefix. */
55
+ export declare function managedByOf(labels: Record<string, unknown> | null | undefined): string | undefined;
32
56
  /** A create's labels: the declaration, the record of its keys when it declares any, and the stamps. */
33
57
  export declare function createLabels(declared: Record<string, string> | undefined, stamps?: Record<string, string>): Record<string, string>;
34
58
  /**
35
59
  * The merge patch that brings the live labels to the declaration, or `undefined` when they already agree: a
36
60
  * declared value that differs, a key an earlier apply declared and this one does not (as `null`), the record
37
- * of declared keys, and the stamps. A live key no apply declared is neither sent nor drift.
61
+ * of declared keys, and the stamps. A live key no apply declared is neither sent nor drift. A marker found under
62
+ * the old prefix is moved: its old key is sent as `null`, and its new key carries the stamp, or the old value
63
+ * when nothing stamps it.
38
64
  */
39
65
  export declare function labelsPatch(declared: Record<string, string> | undefined, live: Record<string, string> | null | undefined, stamps?: Record<string, string>): Record<string, string | null> | undefined;
@@ -4,7 +4,13 @@
4
4
  * Reserving the whole prefix rather than one literal key is what makes the ownership marker unforgeable and
5
5
  * leaves room for the other markers `apply` writes without a second rule.
6
6
  */
7
- export const LABEL_KEY_PREFIX = "sfdk.";
7
+ export const LABEL_KEY_PREFIX = "orchestra.";
8
+ /**
9
+ * The prefix `apply` wrote before the rename. A factory applied then carries its markers under it until its next
10
+ * apply moves them, so every marker is read under both prefixes, the new key winning, and both stay reserved.
11
+ */
12
+ export const OLD_LABEL_KEY_PREFIX = "sfdk.";
13
+ const RESERVED_LABEL_KEY_PREFIXES = [LABEL_KEY_PREFIX, OLD_LABEL_KEY_PREFIX];
8
14
  /**
9
15
  * Stamped on every resource `apply` creates or adopts among the kinds whose API request exposes `labels`.
10
16
  * Its value is the root factory name, which is always present — even for a factory that declares no team — so a CLI
@@ -24,16 +30,17 @@ export function rejectTemplateBraces(map, where) {
24
30
  }
25
31
  /**
26
32
  * A synth-time rejection rather than an apply-time overwrite on purpose: a declared key that could shadow
27
- * `sfdk.managedBy` would make the ownership marker forgeable and make a re-apply fight itself over the same
28
- * key. Refusing at the authoring boundary leaves the marker exactly one writer.
33
+ * the managed-by marker, under either prefix, would make the ownership marker forgeable and make a re-apply
34
+ * fight itself over the same key. Refusing at the authoring boundary leaves the marker exactly one writer.
29
35
  */
30
36
  export function rejectReservedLabelKeys(map, where) {
31
37
  if (map === undefined)
32
38
  return;
33
39
  for (const key of Object.keys(map)) {
34
- if (key.startsWith(LABEL_KEY_PREFIX)) {
40
+ const prefix = reservedPrefixOf(key);
41
+ if (prefix !== undefined) {
35
42
  throw new Error(`Label key ${JSON.stringify(key)} in ${where} uses the reserved ` +
36
- `"${LABEL_KEY_PREFIX}" prefix, which belongs to orchestra itself (it is how apply marks the ` +
43
+ `"${prefix}" prefix, which belongs to orchestra itself (it is how apply marks the ` +
37
44
  `resources it manages, e.g. ${JSON.stringify(MANAGED_BY_LABEL_KEY)}). Rename the key to ` +
38
45
  `anything outside that prefix.`);
39
46
  }
@@ -57,6 +64,51 @@ export function stampManagedBy(labels, factory) {
57
64
  * tells a key the declaration dropped, which it removes, from a key someone else set, which it leaves alone.
58
65
  */
59
66
  export const DECLARED_LABELS_KEY = `${LABEL_KEY_PREFIX}declaredLabels`;
67
+ /**
68
+ * Set on a gateway key's paired Secret while a mint is in flight, so a mint whose token write never landed is
69
+ * seen and refused by name rather than read as a usable Secret.
70
+ */
71
+ export const PENDING_GATEWAY_KEY_TOKEN_LABEL_KEY = `${LABEL_KEY_PREFIX}pendingGatewayKeyToken`;
72
+ const MARKER_KEYS = [MANAGED_BY_LABEL_KEY, DECLARED_LABELS_KEY, PENDING_GATEWAY_KEY_TOKEN_LABEL_KEY];
73
+ /** The key a marker had under the old prefix. */
74
+ export function oldMarkerKey(key) {
75
+ return OLD_LABEL_KEY_PREFIX + key.slice(LABEL_KEY_PREFIX.length);
76
+ }
77
+ /** A marker's value: under its key, else under the old prefix's, else `undefined`. */
78
+ export function readMarker(labels, key) {
79
+ const value = labels?.[key] ?? labels?.[oldMarkerKey(key)];
80
+ return typeof value === "string" ? value : undefined;
81
+ }
82
+ /** A merge patch removing each marker these labels carry under the old prefix. */
83
+ export function oldMarkersCleared(labels) {
84
+ return Object.fromEntries(MARKER_KEYS.map(oldMarkerKey)
85
+ .filter((key) => labels?.[key] !== undefined)
86
+ .map((key) => [key, null]));
87
+ }
88
+ /**
89
+ * The merge patch that moves each marker found under the old prefix, for a resource whose labels apply does not
90
+ * otherwise reconcile: the old key sent as `null`, and the new key given the old value unless it is already
91
+ * live. `undefined` when no marker is under the old prefix.
92
+ */
93
+ export function oldMarkersMoved(labels) {
94
+ const patch = {};
95
+ for (const key of MARKER_KEYS) {
96
+ const old = labels?.[oldMarkerKey(key)];
97
+ if (old === undefined)
98
+ continue;
99
+ patch[oldMarkerKey(key)] = null;
100
+ if (labels?.[key] === undefined && typeof old === "string")
101
+ patch[key] = old;
102
+ }
103
+ return Object.keys(patch).length > 0 ? patch : undefined;
104
+ }
105
+ /** The factory whose apply stamped these labels, read under either prefix. */
106
+ export function managedByOf(labels) {
107
+ return readMarker(labels, MANAGED_BY_LABEL_KEY);
108
+ }
109
+ function reservedPrefixOf(key) {
110
+ return RESERVED_LABEL_KEY_PREFIXES.find((prefix) => key.startsWith(prefix));
111
+ }
60
112
  /** A create's labels: the declaration, the record of its keys when it declares any, and the stamps. */
61
113
  export function createLabels(declared, stamps = {}) {
62
114
  return { ...withDeclaredKeys(declared ?? {}), ...stamps };
@@ -64,7 +116,9 @@ export function createLabels(declared, stamps = {}) {
64
116
  /**
65
117
  * The merge patch that brings the live labels to the declaration, or `undefined` when they already agree: a
66
118
  * declared value that differs, a key an earlier apply declared and this one does not (as `null`), the record
67
- * of declared keys, and the stamps. A live key no apply declared is neither sent nor drift.
119
+ * of declared keys, and the stamps. A live key no apply declared is neither sent nor drift. A marker found under
120
+ * the old prefix is moved: its old key is sent as `null`, and its new key carries the stamp, or the old value
121
+ * when nothing stamps it.
68
122
  */
69
123
  export function labelsPatch(declared, live, stamps = {}) {
70
124
  const current = live ?? {};
@@ -80,6 +134,14 @@ export function labelsPatch(declared, live, stamps = {}) {
80
134
  }
81
135
  if (!(DECLARED_LABELS_KEY in desired) && DECLARED_LABELS_KEY in current)
82
136
  patch[DECLARED_LABELS_KEY] = null;
137
+ for (const key of MARKER_KEYS) {
138
+ const old = oldMarkerKey(key);
139
+ if (!(old in current))
140
+ continue;
141
+ patch[old] = null;
142
+ if (key !== DECLARED_LABELS_KEY && !(key in desired) && !(key in current))
143
+ patch[key] = current[old];
144
+ }
83
145
  return Object.keys(patch).length > 0 ? patch : undefined;
84
146
  }
85
147
  function withDeclaredKeys(declared) {
@@ -88,13 +150,13 @@ function withDeclaredKeys(declared) {
88
150
  }
89
151
  /** A malformed record removes nothing, and a reserved key is never removed through it. */
90
152
  function previouslyDeclaredKeys(live) {
91
- const record = live[DECLARED_LABELS_KEY];
153
+ const record = readMarker(live, DECLARED_LABELS_KEY);
92
154
  if (record === undefined)
93
155
  return [];
94
156
  try {
95
157
  const keys = JSON.parse(record);
96
158
  return Array.isArray(keys)
97
- ? keys.filter((key) => typeof key === "string" && !key.startsWith(LABEL_KEY_PREFIX))
159
+ ? keys.filter((key) => typeof key === "string" && reservedPrefixOf(key) === undefined)
98
160
  : [];
99
161
  }
100
162
  catch {
@@ -10,13 +10,12 @@ import { createHash } from "node:crypto";
10
10
  import { CONNECTOR_SECRET_WIRE_FIELDS } from "../constructs/connector.js";
11
11
  import { isEnvImportMarker, parseEnvImportMarker } from "../constructs/references.js";
12
12
  import { gatewayKeyRefOf } from "../core/gateway-key-recipe.js";
13
- import { labelsPatch, MANAGED_BY_LABEL_KEY } from "../core/labels.js";
13
+ import { labelsPatch, MANAGED_BY_LABEL_KEY, oldMarkersMoved, PENDING_GATEWAY_KEY_TOKEN_LABEL_KEY, readMarker, } from "../core/labels.js";
14
14
  import { isRefToken, refToken } from "../core/tokens.js";
15
15
  import { createTeamResolver, mapError, modelSettingsUnchanged, standbyCapacityUnchanged, platformErrorMessage, } from "./apply-core.js";
16
16
  import { httpStatusOf } from "./client.js";
17
17
  import { describeRefusal, describeStageEffects, leftoverRefusals, pipelineChange, pipelineUnchanged, previewPipelineChange, resolveGateReferences, workflowStagesAfterApply, } from "./pipeline-reconcile.js";
18
18
  import { describeEntryChange, describeOverlaps, entriesUnchanged, pipelineEntryChange, proposedWorkflows, } from "./pipeline-workflow-reconcile.js";
19
- import { PENDING_GATEWAY_KEY_TOKEN_LABEL_KEY } from "./gateway-key-reconcile.js";
20
19
  import { scanModelProviderKeys } from "./model-provider-key-reconcile.js";
21
20
  import { computeProviderPatch, } from "./compute-provider-reconcile.js";
22
21
  import { paginate } from "./pages.js";
@@ -25,7 +24,7 @@ import { reservedProviderSecretIds } from "./preflight.js";
25
24
  import { gatewayKeyPairRefusal, refusalOf, } from "./refusals.js";
26
25
  import { connectionUnchanged, environmentUnchanged, getBySlugOrUndefined, subsetDeepEqual, } from "./reconcile.js";
27
26
  import { removedConfigurationNotice } from "./connector-configuration.js";
28
- import { containsCommandLineRecipe, declaredSecretHosts, metadataHostsChange, metadataInjectionChange, pendingCredentialWrite, replacesLiveCredentialValue, resolveSecretValueSource, rootEnvName, } from "./secrets-apply.js";
27
+ import { containsCommandLineRecipe, declaredSecretInjection, metadataInjectionChange, pendingCredentialWrite, replacesLiveCredentialValue, resolveSecretValueSource, rootEnvName, } from "./secrets-apply.js";
29
28
  /** Overridable for tests. */
30
29
  export const ORPHAN_SCAN_CAP = 5;
31
30
  /**
@@ -428,20 +427,30 @@ async function diffSecret(resource, pass) {
428
427
  }
429
428
  return entry(resource, "no-change", unsetNote, marks);
430
429
  }
431
- /** Value-free: a recipe and a host list are shape, never credential material. */
430
+ /** Value-free: a recipe and its host list are shape, never credential material. */
432
431
  const INJECTION_RECIPE_CHANGE = "injection recipe changes";
433
432
  const HOSTS_CHANGE = "hosts change";
433
+ const OWNERSHIP_LABELS_MOVE = "ownership labels move to the orchestra. prefix";
434
434
  function secretMetadataDetail(resource, live) {
435
435
  const details = [
436
- ...(metadataInjectionChange(declaredInjection(resource), live.injection) !== undefined
437
- ? [INJECTION_RECIPE_CHANGE]
438
- : []),
439
- ...(metadataHostsChange(declaredSecretHosts(resource.payload), live.hosts) !== undefined ? [HOSTS_CHANGE] : []),
436
+ ...secretRecipeDetails(resource, live),
437
+ ...(oldMarkersMoved(live.labels) !== undefined ? [OWNERSHIP_LABELS_MOVE] : []),
440
438
  ];
441
439
  return details.length > 0 ? details.join("; ") : undefined;
442
440
  }
443
- function declaredInjection(resource) {
444
- return resource.payload.injection;
441
+ function secretRecipeDetails(resource, live) {
442
+ const declared = declaredSecretInjection(resource.payload);
443
+ if (metadataInjectionChange(declared, live.injection) === undefined)
444
+ return [];
445
+ if (declared === undefined || live.injection === undefined)
446
+ return [INJECTION_RECIPE_CHANGE];
447
+ const { hosts: desiredHosts = [], ...desiredRecipe } = declared;
448
+ const { hosts: liveHosts = [], ...liveRecipe } = live.injection;
449
+ const sameHosts = desiredHosts.length === liveHosts.length && desiredHosts.every((host, index) => host === liveHosts[index]);
450
+ return [
451
+ ...(metadataInjectionChange(desiredRecipe, liveRecipe) !== undefined ? [INJECTION_RECIPE_CHANGE] : []),
452
+ ...(sameHosts ? [] : [HOSTS_CHANGE]),
453
+ ];
445
454
  }
446
455
  function withDetail(note, detail) {
447
456
  return note === undefined ? detail : `${note}; ${detail}`;
@@ -480,7 +489,7 @@ async function diffGatewayKey(resource, plan, pass) {
480
489
  return entry(resource, keyUnchanged(body, live) ? "no-change" : "update", undefined, marks);
481
490
  }
482
491
  function hasPendingMarker(secret) {
483
- return secret?.labels?.[PENDING_GATEWAY_KEY_TOKEN_LABEL_KEY] !== undefined;
492
+ return readMarker(secret?.labels, PENDING_GATEWAY_KEY_TOKEN_LABEL_KEY) !== undefined;
484
493
  }
485
494
  /**
486
495
  * Mirrors apply's `pairedSecretResource` lookup but returns `undefined` instead of throwing when the plan
@@ -630,6 +639,16 @@ async function diffEnvironment(resource, pass) {
630
639
  if (typeof providerRef === "string" && providerRef.length > 0) {
631
640
  launchSpec.computeProviderId = await resolveIdForDiff(providerRef, "compute-provider", pass);
632
641
  }
642
+ // Each credential reference an env value or a default names, resolved to the id apply writes.
643
+ const env = (launchSpec.env ?? {});
644
+ for (const [name, value] of Object.entries(env)) {
645
+ if (!value.startsWith("orchestra:"))
646
+ continue;
647
+ env[name] = `orchestra:${await credentialReferenceForDiff(value.slice("orchestra:".length), pass)}`;
648
+ }
649
+ for (const entry of (body.credentials ?? [])) {
650
+ entry.credential = await credentialReferenceForDiff(String(entry.credential), pass);
651
+ }
633
652
  const bound = pullSecrets.length === 0
634
653
  ? null
635
654
  : verdictFor(resource, live, {
@@ -929,6 +948,14 @@ async function orphanReport(plan, apis, cap) {
929
948
  function refKeyOf(resource) {
930
949
  return refToken(resource.kind, resource.path.split("/"));
931
950
  }
951
+ /** `secret/<ref>` or `connection/<ref>` with its ref resolved as {@link resolveIdForDiff} resolves one. */
952
+ async function credentialReferenceForDiff(text, pass) {
953
+ const match = /^(secret|connection)\/(.+)$/.exec(text);
954
+ if (match === null)
955
+ return text;
956
+ const kind = match[1];
957
+ return `${kind}/${await resolveIdForDiff(match[2], kind, pass)}`;
958
+ }
932
959
  /** Lenient: any miss leaves the ref unchanged, so the compare simply reports `update`. */
933
960
  async function resolveIdForDiff(ref, literalKind, pass) {
934
961
  if (isRefToken(ref)) {
@@ -8,7 +8,7 @@ import { PreflightError } from "./preflight.js";
8
8
  * second column — and is removed by the final token PATCH. It lives in a leaf module because the aggregate
9
9
  * refusal classifier's rotation message names it too.
10
10
  */
11
- export { PENDING_GATEWAY_KEY_TOKEN_LABEL_KEY } from "./refusals.js";
11
+ export { PENDING_GATEWAY_KEY_TOKEN_LABEL_KEY } from "../core/labels.js";
12
12
  /**
13
13
  * The platform requires a non-empty `value` on create and the token does not exist yet, so a reservation
14
14
  * writes this inert literal and the final PATCH replaces it. The Secret is detectably pending for exactly as
@@ -38,7 +38,6 @@ export interface LiveSecretRow {
38
38
  id: string;
39
39
  teamId?: string;
40
40
  injection?: InjectionRecipe;
41
- hosts?: string[];
42
41
  labels?: Record<string, string>;
43
42
  }
44
43
  /** Value-free. */
@@ -13,17 +13,17 @@
13
13
  import { gatewayKeyRefOf } from "../core/gateway-key-recipe.js";
14
14
  import { refToken } from "../core/tokens.js";
15
15
  import { credentialValueWritten, driftedFields, failing, getBySlugOrUndefined, pushOutcome, stampTeam, } from "./apply-core.js";
16
- import { PENDING_GATEWAY_KEY_TOKEN_LABEL_KEY, refuseIf } from "./refusals.js";
16
+ import { refuseIf } from "./refusals.js";
17
17
  import { httpStatusOf } from "./client.js";
18
18
  import { PreflightError } from "./preflight.js";
19
- import { createLabels, labelsPatch, MANAGED_BY_LABEL_KEY } from "../core/labels.js";
20
- import { declaredSecretHosts, metadataHostsChange, metadataInjectionChange, pendingSecretWrite, reconcileInjection, } from "./secrets-apply.js";
19
+ import { createLabels, labelsPatch, MANAGED_BY_LABEL_KEY, oldMarkersCleared, oldMarkersMoved, PENDING_GATEWAY_KEY_TOKEN_LABEL_KEY, readMarker, } from "../core/labels.js";
20
+ import { declaredSecretInjection, metadataInjectionChange, pendingSecretWrite, reconcileInjection, } from "./secrets-apply.js";
21
21
  /**
22
22
  * The reserved marker rides the ordinary labels wire field — never smuggled into the write-only value or a
23
23
  * second column — and is removed by the final token PATCH. It lives in a leaf module because the aggregate
24
24
  * refusal classifier's rotation message names it too.
25
25
  */
26
- export { PENDING_GATEWAY_KEY_TOKEN_LABEL_KEY } from "./refusals.js";
26
+ export { PENDING_GATEWAY_KEY_TOKEN_LABEL_KEY } from "../core/labels.js";
27
27
  /**
28
28
  * The platform requires a non-empty `value` on create and the token does not exist yet, so a reservation
29
29
  * writes this inert literal and the final PATCH replaces it. The Secret is detectably pending for exactly as
@@ -149,14 +149,17 @@ async function applySteadyGatewayKey(decision, apis, outcomes, applied) {
149
149
  action = "updated";
150
150
  }
151
151
  pushOutcome(outcomes, applied, decision.resource, live.id, action);
152
- // The paired Secret needs no value write in steady state, but a recipe or hosts edit still reconciles
152
+ // The paired Secret needs no value write in steady state, but a recipe edit, its hosts included, still reconciles
153
153
  // through a value-less PATCH — exactly what `applySecrets` would have done had this lane not owned it.
154
154
  const liveSecret = decision.liveSecret;
155
155
  const injectionChange = metadataInjectionChange(secretInjection(decision.secretResource), liveSecret.injection);
156
- const hostsChange = metadataHostsChange(declaredSecretHosts(decision.secretResource.payload), liveSecret.hosts);
156
+ const labelsMove = oldMarkersMoved(liveSecret.labels);
157
157
  let secretAction = "unchanged";
158
- if (injectionChange !== undefined || hostsChange !== undefined) {
159
- const body = { ...injectionChange, ...hostsChange };
158
+ if (injectionChange !== undefined || labelsMove !== undefined) {
159
+ const body = {
160
+ ...injectionChange,
161
+ ...(labelsMove !== undefined ? { labels: labelsMove } : {}),
162
+ };
160
163
  await failing(() => apis.secrets.updateSecret({ id: liveSecret.id, updateSecretRequest: body }), { kind: "secret", slug: decision.secretSlug, op: "update" }, outcomes);
161
164
  secretAction = "updated";
162
165
  }
@@ -174,9 +177,12 @@ async function mintGatewayKey(decision, apis, outcomes, applied, tokens, factory
174
177
  const refKey = refToken("gateway-key", decision.resource.path.split("/"));
175
178
  // The FINAL patch removes the pending marker and stamps ownership; the merge keeps every caller-authored entry
176
179
  // an adopted Secret carries.
177
- const finalLabels = { [MANAGED_BY_LABEL_KEY]: factory, [PENDING_GATEWAY_KEY_TOKEN_LABEL_KEY]: null };
180
+ const finalLabels = {
181
+ [MANAGED_BY_LABEL_KEY]: factory,
182
+ [PENDING_GATEWAY_KEY_TOKEN_LABEL_KEY]: null,
183
+ ...oldMarkersCleared(decision.liveSecret?.labels),
184
+ };
178
185
  const declaredInjection = secretInjection(decision.secretResource);
179
- const declaredHosts = declaredSecretHosts(decision.secretResource.payload);
180
186
  // (1) Make the paired Secret detectably pending BEFORE the mint.
181
187
  let secretId;
182
188
  if (decision.action === "mint-new") {
@@ -188,7 +194,6 @@ async function mintGatewayKey(decision, apis, outcomes, applied, tokens, factory
188
194
  labels: { [PENDING_GATEWAY_KEY_TOKEN_LABEL_KEY]: decision.slug },
189
195
  ...placement,
190
196
  ...(declaredInjection !== undefined ? { injection: declaredInjection } : {}),
191
- ...(declaredHosts !== undefined ? { hosts: declaredHosts } : {}),
192
197
  };
193
198
  const created = await failing(() => apis.secrets.createSecret({ createSecretRequest: createBody }), { kind: "secret", slug: decision.secretSlug, op: "create" }, outcomes);
194
199
  secretId = created.id;
@@ -202,6 +207,7 @@ async function mintGatewayKey(decision, apis, outcomes, applied, tokens, factory
202
207
  await patchSecretPendingMarker(apis, outcomes, decision.secretSlug, secretId, {
203
208
  [MANAGED_BY_LABEL_KEY]: factory,
204
209
  [PENDING_GATEWAY_KEY_TOKEN_LABEL_KEY]: decision.slug,
210
+ ...oldMarkersCleared(liveSecret.labels),
205
211
  });
206
212
  }
207
213
  }
@@ -237,8 +243,6 @@ async function mintGatewayKey(decision, apis, outcomes, applied, tokens, factory
237
243
  // No team: a `mint-new` reservation already placed the create, and an adopted or resumed Secret is
238
244
  // only here because the decide phase found it already in the declared team.
239
245
  ...(injection !== undefined ? { injection } : {}),
240
- // A `mint-new` reservation already carried the hosts on its create.
241
- ...(decision.action !== "mint-new" && declaredHosts !== undefined ? { hosts: declaredHosts } : {}),
242
246
  },
243
247
  }), { kind: "secret", slug: decision.secretSlug, op: "update" }, outcomes);
244
248
  // The pinned asymmetry between the pair's two rows: the `gateway-key` row is the MINT, whose sensitivity
@@ -277,7 +281,7 @@ function pairedSecretResource(plan, key) {
277
281
  return paired[0];
278
282
  }
279
283
  function hasPendingMarker(secret) {
280
- return secret.labels?.[PENDING_GATEWAY_KEY_TOKEN_LABEL_KEY] !== undefined;
284
+ return readMarker(secret.labels, PENDING_GATEWAY_KEY_TOKEN_LABEL_KEY) !== undefined;
281
285
  }
282
286
  /**
283
287
  * Exactly the fields the update request accepts and the construct emits. `monthlyCreditCap` is present only
@@ -293,5 +297,5 @@ function declaredKeyState(payload) {
293
297
  };
294
298
  }
295
299
  function secretInjection(secretResource) {
296
- return secretResource.payload.injection;
300
+ return declaredSecretInjection(secretResource.payload);
297
301
  }
@@ -11,7 +11,7 @@
11
11
  import { OPEN_CATEGORIES } from "../constructs/pipeline.js";
12
12
  import { resolveConditionReferences, resolveStageInField } from "./condition-references.js";
13
13
  import { wireNode } from "../constructs/workflow-filter.js";
14
- import { createLabels, labelsPatch, MANAGED_BY_LABEL_KEY } from "../core/labels.js";
14
+ import { createLabels, labelsPatch, managedByOf } from "../core/labels.js";
15
15
  import { refToken } from "../core/tokens.js";
16
16
  import { exactDeepEqual, failing, getBySlugOrUndefined, stampTeam, } from "./apply-core.js";
17
17
  import { PreflightError } from "./preflight-errors.js";
@@ -242,7 +242,7 @@ export async function workflowStagesAfterApply(pipeline, live, resources, factor
242
242
  const versions = [
243
243
  ...declaredEntries.filter((entry) => refs.has(entry.workflow)).map((entry) => entry.version),
244
244
  ...liveEntries
245
- .filter((entry) => entry.labels[MANAGED_BY_LABEL_KEY] !== factory && refs.has(entry.workflow.id))
245
+ .filter((entry) => managedByOf(entry.labels) !== factory && refs.has(entry.workflow.id))
246
246
  .map((entry) => entry.version ?? null),
247
247
  ];
248
248
  if (versions.some((version) => typeof version === "number"))
@@ -1,4 +1,4 @@
1
- // A pipeline's workflow entries. Apply owns only the entries it stamped with this factory's `sfdk.managedBy`:
1
+ // A pipeline's workflow entries. Apply owns only the entries it stamped with this factory's managed-by marker:
2
2
  // each declared entry takes a stamped live entry of its workflow, an identical one first and then the next in
3
3
  // order, and a changed one is patched field by field; a declared entry left over is added with the stamp, and
4
4
  // a stamped entry nothing declares is removed. Every other entry stays where it is and is named, since an
@@ -9,11 +9,11 @@
9
9
  // new rules before its entry says so; the entries phase then sets each to its declared version.
10
10
  import { APPLIED_VERSION } from "../constructs/pipeline.js";
11
11
  import { wireNode } from "../constructs/workflow-filter.js";
12
- import { MANAGED_BY_LABEL_KEY } from "../core/labels.js";
12
+ import { MANAGED_BY_LABEL_KEY, managedByOf, oldMarkersCleared } from "../core/labels.js";
13
13
  import { exactDeepEqual, failing, getBySlugOrUndefined } from "./apply-core.js";
14
14
  import { rawJsonBody } from "./raw-json-request.js";
15
15
  export function entryChange(declared, live, factory) {
16
- const stamped = live.filter((entry) => entry.labels[MANAGED_BY_LABEL_KEY] === factory);
16
+ const stamped = live.filter((entry) => managedByOf(entry.labels) === factory);
17
17
  const taken = new Set();
18
18
  const take = (entry, fits) => {
19
19
  const found = stamped.find((candidate) => !taken.has(candidate.id) && names(entry, candidate) && fits(candidate));
@@ -21,18 +21,18 @@ export function entryChange(declared, live, factory) {
21
21
  taken.add(found.id);
22
22
  return found;
23
23
  };
24
- const exact = declared.map((entry) => take(entry, (candidate) => patchOf(entry, candidate) === undefined));
24
+ const exact = declared.map((entry) => take(entry, (candidate) => patchOf(entry, candidate, factory) === undefined));
25
25
  const matched = declared.map((entry, index) => exact[index] ?? take(entry, () => true));
26
26
  return {
27
27
  declared: declared.map((entry, index) => ({ entry, ...(matched[index] ? { liveId: matched[index].id } : {}) })),
28
28
  create: declared.filter((_, index) => matched[index] === undefined),
29
29
  update: declared.flatMap((entry, index) => {
30
30
  const live = matched[index];
31
- const patch = live !== undefined ? patchOf(entry, live) : undefined;
31
+ const patch = live !== undefined ? patchOf(entry, live, factory) : undefined;
32
32
  return live !== undefined && patch !== undefined ? [{ id: live.id, patch }] : [];
33
33
  }),
34
34
  remove: stamped.filter((entry) => !taken.has(entry.id)),
35
- unmanaged: live.filter((entry) => entry.labels[MANAGED_BY_LABEL_KEY] !== factory),
35
+ unmanaged: live.filter((entry) => managedByOf(entry.labels) !== factory),
36
36
  };
37
37
  }
38
38
  export function entriesUnchanged(change) {
@@ -87,7 +87,7 @@ export async function applyEntriesPhase(apis, pipelines, versions, factory, outc
87
87
  */
88
88
  export async function pinFollowingEntries(apis, pipeline, workflow, version, factory) {
89
89
  const live = await getBySlugOrUndefined(() => apis.pipelines.getPipeline({ id: pipeline }));
90
- const following = (live?.workflows ?? []).filter((entry) => entry.labels[MANAGED_BY_LABEL_KEY] === factory && entry.workflow.id === workflow.id && entry.version === undefined);
90
+ const following = (live?.workflows ?? []).filter((entry) => managedByOf(entry.labels) === factory && entry.workflow.id === workflow.id && entry.version === undefined);
91
91
  for (const entry of following)
92
92
  await patchEntry(apis, live.id, entry.id, { version });
93
93
  return following.length;
@@ -165,9 +165,15 @@ export function describeOverlaps(proposedList, overlaps) {
165
165
  function names(entry, live) {
166
166
  return entry.workflow === live.workflow.slug || entry.workflow === live.workflow.id;
167
167
  }
168
- /** The fields a patch must send to make [live] the declared entry, null resetting; `undefined` when none. */
169
- function patchOf(entry, live) {
168
+ /**
169
+ * The fields a patch must send to make [live] the declared entry, null resetting; `undefined` when none. An entry
170
+ * stamped under the old prefix has its stamp moved.
171
+ */
172
+ function patchOf(entry, live, factory) {
170
173
  const patch = {};
174
+ const moved = oldMarkersCleared(live.labels);
175
+ if (Object.keys(moved).length > 0)
176
+ patch.labels = { ...moved, [MANAGED_BY_LABEL_KEY]: factory };
171
177
  if (entry.version !== (live.version ?? null))
172
178
  patch.version = entry.version;
173
179
  const where = entry.where === null ? null : wireNode(entry.where);