@noodleseed/one 0.165.0 → 0.167.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 (126) hide show
  1. package/dist/commands/author-loop.d.ts.map +1 -1
  2. package/dist/commands/author-loop.js +18 -3
  3. package/dist/commands/author-loop.js.map +1 -1
  4. package/dist/commands/deploy-ops.d.ts.map +1 -1
  5. package/dist/commands/deploy-ops.js +21 -2
  6. package/dist/commands/deploy-ops.js.map +1 -1
  7. package/dist/commands/deploy-status-ops.d.ts +2 -0
  8. package/dist/commands/deploy-status-ops.d.ts.map +1 -1
  9. package/dist/commands/deploy-status-ops.js +27 -0
  10. package/dist/commands/deploy-status-ops.js.map +1 -1
  11. package/dist/control-plane-request.d.ts +2 -0
  12. package/dist/control-plane-request.d.ts.map +1 -1
  13. package/dist/control-plane-request.js +18 -0
  14. package/dist/control-plane-request.js.map +1 -1
  15. package/dist/deploy.d.ts +2 -1
  16. package/dist/deploy.d.ts.map +1 -1
  17. package/dist/deploy.js +15 -0
  18. package/dist/deploy.js.map +1 -1
  19. package/dist/dev.d.ts +2 -0
  20. package/dist/dev.d.ts.map +1 -1
  21. package/dist/dev.js +12 -1
  22. package/dist/dev.js.map +1 -1
  23. package/dist/devtools-auth-session.d.ts +5 -0
  24. package/dist/devtools-auth-session.d.ts.map +1 -1
  25. package/dist/devtools-auth-session.js +41 -2
  26. package/dist/devtools-auth-session.js.map +1 -1
  27. package/dist/devtools-auth-types.d.ts +2 -1
  28. package/dist/devtools-auth-types.d.ts.map +1 -1
  29. package/dist/devtools-auth-types.js.map +1 -1
  30. package/dist/devtools-auth-ui.d.ts +1 -1
  31. package/dist/devtools-auth-ui.d.ts.map +1 -1
  32. package/dist/devtools-auth-ui.js +13 -5
  33. package/dist/devtools-auth-ui.js.map +1 -1
  34. package/dist/devtools-harness.d.ts +1 -0
  35. package/dist/devtools-harness.d.ts.map +1 -1
  36. package/dist/devtools-harness.js +12 -7
  37. package/dist/devtools-harness.js.map +1 -1
  38. package/dist/devtools-preview.d.ts +1 -0
  39. package/dist/devtools-preview.d.ts.map +1 -1
  40. package/dist/devtools-preview.js +21 -6
  41. package/dist/devtools-preview.js.map +1 -1
  42. package/dist/devtools-rpc-forwarder.d.ts +1 -0
  43. package/dist/devtools-rpc-forwarder.d.ts.map +1 -1
  44. package/dist/devtools-rpc-forwarder.js +2 -1
  45. package/dist/devtools-rpc-forwarder.js.map +1 -1
  46. package/dist/preview-session.d.ts +1 -0
  47. package/dist/preview-session.d.ts.map +1 -1
  48. package/dist/preview-session.js +1 -0
  49. package/dist/preview-session.js.map +1 -1
  50. package/node_modules/@noodle-borg/agent-kit/dist/generated/example-files.js +3 -3
  51. package/node_modules/@noodle-borg/agent-kit/dist/skill-authoring-auth-ref.js +5 -2
  52. package/node_modules/@noodle-borg/agent-kit/package.json +1 -1
  53. package/node_modules/@noodle-borg/agent-packaging/dist/product-skill-types.d.ts +1 -0
  54. package/node_modules/@noodle-borg/agent-packaging/dist/product-skill-validation.js +4 -0
  55. package/node_modules/@noodle-borg/app-package/dist/schema.js +1 -0
  56. package/node_modules/@noodle-borg/app-package/dist/types.d.ts +1 -0
  57. package/node_modules/@noodle-borg/authoring/dist/server.d.ts +3 -1
  58. package/node_modules/@noodle-borg/authoring/dist/server.js +7 -6
  59. package/node_modules/@noodle-borg/capabilities/dist/index.js +7 -0
  60. package/node_modules/@noodle-borg/cli-catalog/dist/catalog-data-local-authoring.js +8 -0
  61. package/node_modules/@noodle-borg/compiler/dist/app-package/compile.js +9 -1
  62. package/node_modules/@noodle-borg/compiler/dist/artifact/catalog-emission.js +3 -0
  63. package/node_modules/@noodle-borg/compiler/dist/artifact/types.d.ts +2 -0
  64. package/node_modules/@noodle-borg/compiler/dist/artifact/version.d.ts +2 -1
  65. package/node_modules/@noodle-borg/compiler/dist/artifact/version.js +2 -1
  66. package/node_modules/@noodle-borg/compiler/dist/manifest/schema.d.ts +5 -0
  67. package/node_modules/@noodle-borg/compiler/dist/manifest/schema.js +12 -2
  68. package/node_modules/@noodle-borg/module/dist/customer-auth-access.d.ts +7 -0
  69. package/node_modules/@noodle-borg/module/dist/customer-auth-access.js +6 -0
  70. package/node_modules/@noodle-borg/module/dist/index.d.ts +1 -0
  71. package/node_modules/@noodle-borg/module/dist/index.js +1 -0
  72. package/node_modules/@noodle-borg/protocol/dist/handlers/tools.js +3 -0
  73. package/node_modules/@noodle-borg/protocol/dist/index.d.ts +1 -0
  74. package/node_modules/@noodle-borg/protocol/dist/index.js +1 -0
  75. package/node_modules/@noodle-borg/protocol/dist/mapping.d.ts +5 -1
  76. package/node_modules/@noodle-borg/protocol/dist/mapping.js +15 -7
  77. package/node_modules/@noodle-borg/protocol/dist/sdk-server.d.ts +2 -0
  78. package/node_modules/@noodle-borg/protocol/dist/tool-discovery.d.ts +14 -0
  79. package/node_modules/@noodle-borg/protocol/dist/tool-discovery.js +12 -0
  80. package/node_modules/@noodle-borg/protocol/dist/v2/server.js +3 -0
  81. package/node_modules/@noodle-borg/service/dist/build-info.js +4 -2
  82. package/node_modules/@noodle-borg/service/dist/customer-auth-audience-binding.js +16 -5
  83. package/node_modules/@noodle-borg/service/dist/deployment-access-mode.js +24 -0
  84. package/node_modules/@noodle-borg/service/dist/deployment-activation-precondition.js +27 -0
  85. package/node_modules/@noodle-borg/service/dist/deployment-authentication.js +19 -0
  86. package/node_modules/@noodle-borg/service/dist/deployment-execution.js +2 -2
  87. package/node_modules/@noodle-borg/service/dist/deployment-record-version.js +55 -0
  88. package/node_modules/@noodle-borg/service/dist/developer-mcp/inspection.js +15 -1
  89. package/node_modules/@noodle-borg/service/dist/registry-access.js +47 -30
  90. package/node_modules/@noodle-borg/service/dist/registry-compile.js +42 -1
  91. package/node_modules/@noodle-borg/service/dist/registry-deploy-transaction.js +105 -15
  92. package/node_modules/@noodle-borg/service/dist/registry-rollback.js +15 -1
  93. package/node_modules/@noodle-borg/service/dist/registry-state.d.ts +3 -6
  94. package/node_modules/@noodle-borg/service/dist/registry-state.js +15 -7
  95. package/node_modules/@noodle-borg/service/dist/registry-status.js +5 -3
  96. package/node_modules/@noodle-borg/service/dist/registry-targets.js +43 -28
  97. package/node_modules/@noodle-borg/service/dist/registry-types.d.ts +4 -2
  98. package/node_modules/@noodle-borg/service/dist/registry.js +68 -72
  99. package/node_modules/@noodle-borg/service/dist/routes/access.js +7 -0
  100. package/node_modules/@noodle-borg/service/dist/routes/auth-doctor-live.js +3 -2
  101. package/node_modules/@noodle-borg/service/dist/routes/control-plane.js +1 -0
  102. package/node_modules/@noodle-borg/service/dist/routes/deploy-preflight.js +2 -2
  103. package/node_modules/@noodle-borg/service/dist/serve-resource-auth.js +1 -1
  104. package/node_modules/@noodle-borg/service/dist/store/in-memory.d.ts +3 -7
  105. package/node_modules/@noodle-borg/service/dist/store/in-memory.js +13 -4
  106. package/node_modules/@noodle-borg/service/dist/store/json-file.d.ts +3 -7
  107. package/node_modules/@noodle-borg/service/dist/store/json-file.js +15 -6
  108. package/node_modules/@noodle-borg/service/dist/store/records.js +15 -2
  109. package/node_modules/@noodle-borg/service/dist/store.d.ts +23 -8
  110. package/node_modules/@noodle-borg/service/package.json +1 -1
  111. package/node_modules/@noodle-borg/transport-http/dist/handler.d.ts +2 -3
  112. package/node_modules/@noodle-borg/transport-http/dist/handler.js +3 -4
  113. package/node_modules/@noodle-borg/transport-http/dist/identity-authorization.d.ts +3 -2
  114. package/node_modules/@noodle-borg/transport-http/dist/identity-authorization.js +23 -12
  115. package/node_modules/@noodle-borg/transport-http/dist/index.d.ts +1 -0
  116. package/node_modules/@noodle-borg/transport-http/dist/serve-request.js +3 -0
  117. package/node_modules/@noodle-borg/transport-http/dist/target-authentication.d.ts +15 -0
  118. package/node_modules/@noodle-borg/transport-http/dist/target-authentication.js +9 -0
  119. package/node_modules/@noodle-borg/wire-contracts/dist/control-plane.d.ts +23 -0
  120. package/node_modules/@noodle-borg/wire-contracts/dist/control-plane.js +34 -4
  121. package/node_modules/@noodle-borg/wire-contracts/dist/index.d.ts +6 -0
  122. package/node_modules/@noodle-borg/wire-contracts/dist/index.js +4 -1
  123. package/node_modules/@noodle-borg/wire-contracts/dist/service-info.d.ts +41 -0
  124. package/node_modules/@noodle-borg/wire-contracts/dist/service-info.js +26 -0
  125. package/package.json +2 -2
  126. package/node_modules/@noodle-borg/service/dist/routes/access-mode.js +0 -7
@@ -1,10 +1,12 @@
1
- import { assertUniqueActiveCustomerAuthAudienceBindings, CustomerAuthAudienceConflictError, } from './customer-auth-audience-binding.js';
1
+ import { assertUniqueActiveCustomerAuthAudienceBindings, CustomerAuthAudienceConflictError, hasExactCustomerAuthProjection, } from './customer-auth-audience-binding.js';
2
+ import { deploymentRecordVersionError } from './deployment-record-version.js';
2
3
  import { deploymentOwnerSubject } from './registry-helpers.js';
3
4
  import { activeRecord, activeRecordVersion } from './registry-state.js';
4
5
  import { tenantDeploymentKey, tryServedTargetFor } from './registry-targets.js';
6
+ import { resolveActiveAccessUpdate } from './store/records.js';
5
7
  /** Registry-facing access transition with customer audience ownership preflight and atomic translation. */
6
8
  export async function updateRegistryAccess(state, tenant, options, loadServed, customerVerifierFactory, hasCustomerAuthConflict) {
7
- if (options.accessMode === 'customers') {
9
+ if (options.accessMode === 'customers' || options.accessMode === 'mixed') {
8
10
  const record = options.serverVersion === undefined
9
11
  ? await activeRecord(state, tenant)
10
12
  : await activeRecordVersion(state, tenant, options.serverVersion);
@@ -12,7 +14,11 @@ export async function updateRegistryAccess(state, tenant, options, loadServed, c
12
14
  const auth = served?.served.artifact.server.auth;
13
15
  if (record !== undefined &&
14
16
  auth !== undefined &&
15
- (await hasCustomerAuthConflict({ ...record, accessMode: 'customers' }, auth))) {
17
+ (await hasCustomerAuthConflict({
18
+ ...record,
19
+ accessMode: options.accessMode,
20
+ ...(options.accessMode === 'mixed' ? { schemaVersion: 2 } : {}),
21
+ }, auth))) {
16
22
  return customerAuthAudienceConflict();
17
23
  }
18
24
  }
@@ -41,17 +47,34 @@ export async function updateActiveAccess(state, tenant, options, loadServed, cus
41
47
  : await activeRecordVersion(state, tenant, options.serverVersion);
42
48
  if (record === undefined)
43
49
  return noActiveDeployment();
50
+ const versionError = deploymentRecordVersionError(record);
51
+ if (versionError !== undefined) {
52
+ return {
53
+ ok: false,
54
+ status: 409,
55
+ code: 'unsupported_deployment_record_version',
56
+ message: versionError.message,
57
+ };
58
+ }
44
59
  const nextAccessMode = options.accessMode;
45
60
  const previousAccessMode = record.accessMode ?? 'owner-only';
46
61
  const previousOwnerSubject = deploymentOwnerSubject(record);
47
62
  const nextOwnerSubject = options.ownerSubject ?? previousOwnerSubject;
48
63
  const accessChanged = previousAccessMode !== nextAccessMode;
49
64
  const ownerChanged = previousOwnerSubject !== nextOwnerSubject;
50
- const served = accessChanged || ownerChanged || nextAccessMode === 'customers'
65
+ const served = accessChanged || ownerChanged || nextAccessMode === 'customers' || nextAccessMode === 'mixed'
51
66
  ? await loadServed(record.deploymentId)
52
67
  : undefined;
53
68
  const serverAuth = served?.served.artifact.server.auth;
54
- if (nextAccessMode === 'customers' && serverAuth === undefined) {
69
+ const schemaVersion = record.schemaVersion === 2 || (nextAccessMode === 'mixed' && serverAuth !== undefined) ? 2 : 1;
70
+ const policyChanged = schemaVersion !== record.schemaVersion;
71
+ if ((nextAccessMode === 'customers' && serverAuth === undefined) ||
72
+ !hasExactCustomerAuthProjection({
73
+ ...record,
74
+ schemaVersion,
75
+ accessMode: nextAccessMode,
76
+ ...(record.serverAuth === undefined && serverAuth !== undefined ? { serverAuth } : {}),
77
+ }, serverAuth)) {
55
78
  return {
56
79
  ok: false,
57
80
  status: 409,
@@ -59,17 +82,23 @@ export async function updateActiveAccess(state, tenant, options, loadServed, cus
59
82
  message: 'Customer access requires server authentication.',
60
83
  };
61
84
  }
62
- if (!accessChanged && !ownerChanged) {
85
+ if (!accessChanged && !ownerChanged && !policyChanged) {
63
86
  const committed = state.store === undefined
64
87
  ? commitInMemoryAccessUpdate(state, tenant, record, {
65
88
  accessMode: nextAccessMode,
66
89
  expectedAccessMode: record.accessMode,
90
+ ...(schemaVersion === 2 ? { schemaVersion: 2 } : {}),
91
+ expectedSchemaVersion: record.schemaVersion,
92
+ expectedManifest: record.manifest,
67
93
  expectedOwnerSubject: previousOwnerSubject,
68
94
  ...(serverAuth !== undefined ? { serverAuth } : {}),
69
95
  })
70
96
  : await state.store.updateActiveAccess(tenant, record.deploymentId, {
71
97
  accessMode: nextAccessMode,
72
98
  expectedAccessMode: record.accessMode,
99
+ ...(schemaVersion === 2 ? { schemaVersion: 2 } : {}),
100
+ expectedSchemaVersion: record.schemaVersion,
101
+ expectedManifest: record.manifest,
73
102
  expectedOwnerSubject: previousOwnerSubject,
74
103
  ...(serverAuth !== undefined ? { serverAuth } : {}),
75
104
  });
@@ -80,6 +109,7 @@ export async function updateActiveAccess(state, tenant, options, loadServed, cus
80
109
  ok: true,
81
110
  changed: false,
82
111
  accessChanged: false,
112
+ policyChanged: false,
83
113
  ownerChanged: false,
84
114
  previousAccessMode,
85
115
  ...(previousOwnerSubject !== undefined ? { previousOwnerSubject } : {}),
@@ -107,8 +137,9 @@ export async function updateActiveAccess(state, tenant, options, loadServed, cus
107
137
  const updated = {
108
138
  ...record,
109
139
  accessMode: nextAccessMode,
140
+ schemaVersion,
110
141
  ...(ownerChanged && nextOwnerSubject !== undefined ? { ownerSubject: nextOwnerSubject } : {}),
111
- ...(nextAccessMode === 'customers' && serverAuth !== undefined ? { serverAuth } : {}),
142
+ ...(serverAuth !== undefined ? { serverAuth } : {}),
112
143
  };
113
144
  const target = tryServedTargetFor(updated, served.served, customerVerifierFactory);
114
145
  if (target === undefined)
@@ -120,6 +151,9 @@ export async function updateActiveAccess(state, tenant, options, loadServed, cus
120
151
  ? { ownerSubject: nextOwnerSubject }
121
152
  : {}),
122
153
  expectedAccessMode: record.accessMode,
154
+ ...(schemaVersion === 2 ? { schemaVersion: 2 } : {}),
155
+ expectedSchemaVersion: record.schemaVersion,
156
+ expectedManifest: record.manifest,
123
157
  expectedOwnerSubject: previousOwnerSubject,
124
158
  ...(serverAuth !== undefined ? { serverAuth } : {}),
125
159
  })
@@ -129,6 +163,9 @@ export async function updateActiveAccess(state, tenant, options, loadServed, cus
129
163
  ? { ownerSubject: nextOwnerSubject }
130
164
  : {}),
131
165
  expectedAccessMode: record.accessMode,
166
+ ...(schemaVersion === 2 ? { schemaVersion: 2 } : {}),
167
+ expectedSchemaVersion: record.schemaVersion,
168
+ expectedManifest: record.manifest,
132
169
  expectedOwnerSubject: previousOwnerSubject,
133
170
  ...(serverAuth !== undefined ? { serverAuth } : {}),
134
171
  });
@@ -140,6 +177,7 @@ export async function updateActiveAccess(state, tenant, options, loadServed, cus
140
177
  ok: true,
141
178
  changed: true,
142
179
  accessChanged,
180
+ policyChanged,
143
181
  ownerChanged,
144
182
  previousAccessMode,
145
183
  ...(previousOwnerSubject !== undefined ? { previousOwnerSubject } : {}),
@@ -147,30 +185,9 @@ export async function updateActiveAccess(state, tenant, options, loadServed, cus
147
185
  };
148
186
  }
149
187
  function commitInMemoryAccessUpdate(state, tenant, observed, input) {
150
- const current = state.records.get(observed.deploymentId);
151
- if (current === undefined ||
152
- !current.active ||
153
- current.archivedAt !== undefined ||
154
- current.orgSlug !== tenant.org ||
155
- current.appSlug !== tenant.app ||
156
- current.environment !== tenant.env ||
157
- current.accessMode !== input.expectedAccessMode ||
158
- deploymentOwnerSubject(current) !== input.expectedOwnerSubject) {
188
+ const committed = resolveActiveAccessUpdate(state.records.get(observed.deploymentId), tenant, input);
189
+ if (committed === undefined)
159
190
  return undefined;
160
- }
161
- if ((current.accessMode ?? 'owner-only') === input.accessMode &&
162
- (input.ownerSubject === undefined || deploymentOwnerSubject(current) === input.ownerSubject) &&
163
- input.accessMode !== 'customers') {
164
- return current;
165
- }
166
- const committed = {
167
- ...current,
168
- accessMode: input.accessMode,
169
- ...(input.ownerSubject !== undefined ? { ownerSubject: input.ownerSubject } : {}),
170
- ...(input.accessMode === 'customers' && input.serverAuth !== undefined
171
- ? { serverAuth: input.serverAuth }
172
- : {}),
173
- };
174
191
  const proposed = new Map(state.records).set(committed.deploymentId, committed);
175
192
  assertUniqueActiveCustomerAuthAudienceBindings([...proposed.values()]);
176
193
  state.records.set(committed.deploymentId, committed);
@@ -4,12 +4,24 @@ import { compileConnectors, delegatedTokenExchangeIdentityErrors, } from '@noodl
4
4
  import { InMemoryConnectorRegistry, resolveManagedOrigins, resolveVariableEnvironment, } from '@noodle-borg/runtime';
5
5
  import { AppPackageSnapshotError, createAppPackageSnapshot, } from './app-package-snapshot.js';
6
6
  import { missingServerConfigErrors } from './assistant-bindings.js';
7
+ import { normalizePersistedConnectorsForCompile } from './connector-normalize.js';
7
8
  import { ManagedConfigBroker } from './credential-broker.js';
8
9
  import { deploymentCredentialBrokerOptions } from './credential-broker-options.js';
10
+ import { deploymentRecordVersionError } from './deployment-record-version.js';
9
11
  import { normalizePersistedManifestForCompile } from './manifest-normalize.js';
10
- import { missingSecretErrors, missingVariableErrors } from './registry-helpers.js';
12
+ import { missingCapabilityErrors, missingSecretErrors, missingVariableErrors, } from './registry-helpers.js';
13
+ import { servedTargetFor } from './registry-targets.js';
11
14
  import { createDeploymentStateConnector, } from './state-connector-factory.js';
12
15
  import { resolveConfigScope, } from './store.js';
16
+ /** Reject unsupported persisted semantics before constructing executable runtime dependencies. */
17
+ export async function compilePersistedRegistryRecord(record, compileTarget) {
18
+ const versionError = deploymentRecordVersionError(record);
19
+ if (versionError !== undefined)
20
+ return { ok: false, errors: [versionError] };
21
+ return compileTarget({ org: record.orgSlug, app: record.appSlug, env: record.environment }, record.manifest, record.connectors === undefined
22
+ ? undefined
23
+ : normalizePersistedConnectorsForCompile(record.connectors), record.hostedAssets, false, record.deploymentId);
24
+ }
13
25
  export async function compileRegistryTarget(context, input) {
14
26
  const { tenant, manifest, connectors, hostedAssets, deploymentId } = input;
15
27
  let catalogConnectors = [];
@@ -216,4 +228,33 @@ export async function compileRegistryTarget(context, input) {
216
228
  ...(appPackageSnapshot !== undefined ? { appPackageSnapshot } : {}),
217
229
  };
218
230
  }
231
+ /** Load and validate persisted semantics before publishing an executable target to caches. */
232
+ export async function loadPersistedRegistryTarget(source, context) {
233
+ const { state } = context;
234
+ const record = typeof source === 'string'
235
+ ? state.store
236
+ ? await state.store.get(source)
237
+ : state.records.get(source)
238
+ : source;
239
+ if (!record ||
240
+ record.archivedAt !== undefined ||
241
+ deploymentRecordVersionError(record) !== undefined)
242
+ return undefined;
243
+ const deploymentId = record.deploymentId;
244
+ const built = await context.compile(record);
245
+ if (!built.ok) {
246
+ throw new Error(`deployment ${deploymentId} failed to recompile (${built.errors.map((e) => e.code).join(',')})`);
247
+ }
248
+ const missingCapabilities = missingCapabilityErrors(built.served.artifact.requirements?.capabilities ?? [], context.capabilities);
249
+ if (missingCapabilities.length > 0) {
250
+ throw new Error(`deployment ${deploymentId} failed capability requirements (${missingCapabilities.map((e) => e.path).join(',')})`);
251
+ }
252
+ if (record.active &&
253
+ (await context.hasCustomerAuthConflict(record, built.served.artifact.server.auth)))
254
+ return undefined;
255
+ const target = servedTargetFor(record, built.served, context.customerVerifierFactory);
256
+ state.servers.set(deploymentId, target);
257
+ state.records.set(deploymentId, record);
258
+ return target;
259
+ }
219
260
  //# sourceMappingURL=registry-compile.js.map
@@ -1,6 +1,10 @@
1
1
  import { normalizeServerVersion } from '@noodle-borg/module';
2
+ import { deploymentAuthenticationFor } from './deployment-authentication.js';
3
+ import { deploymentRecordVersionError } from './deployment-record-version.js';
4
+ import { sameTenantRecord } from './deployment-versioning.js';
2
5
  import { deployAccessRequiresActor, deployerRequiredError, deploymentOwnerSubject, emptyMembershipSourcesError, membershipSourcesRequireOrgMembersError, missingCapabilityErrors, serverAuthRequiredError, } from './registry-helpers.js';
3
- import { idempotentDeploymentId, mintDeploymentId } from './registry-targets.js';
6
+ import { activeRecordVersion } from './registry-state.js';
7
+ import { emptySecretEnvelope, idempotentDeploymentId, mintDeploymentId, } from './registry-targets.js';
4
8
  /** Serialize candidate validation/activation with hierarchical configuration writers. */
5
9
  export async function withDeploymentConfiguration(config, org, work) {
6
10
  if (!config.transactConfig)
@@ -19,6 +23,11 @@ export async function withDeploymentConfiguration(config, org, work) {
19
23
  throw error;
20
24
  }
21
25
  }
26
+ /** Validate the exact destination scope before compilation or publication side effects. */
27
+ export async function deploymentWriteVersionError(state, tenant, serverVersion) {
28
+ const active = await activeDeploymentWriteScope(state, tenant, serverVersion);
29
+ return active === undefined ? undefined : deploymentRecordVersionError(active);
30
+ }
22
31
  export async function preflightRegistryDeploy(input) {
23
32
  const { actor, accessMode, orgMembershipSources } = input.options;
24
33
  if (input.options.serverVersion !== undefined) {
@@ -32,6 +41,9 @@ export async function preflightRegistryDeploy(input) {
32
41
  const artifact = built.ok ? built.served.artifact : built.compiledArtifact;
33
42
  if (artifact !== undefined) {
34
43
  errors.push(...missingCapabilityErrors(artifact.requirements?.capabilities ?? [], input.serviceCapabilities));
44
+ if (accessMode === 'mixed' && artifact.server.auth !== undefined && actor === undefined) {
45
+ errors.push(...deployerRequiredError(accessMode).errors);
46
+ }
35
47
  if (accessMode === 'customers' && artifact.server.auth === undefined) {
36
48
  errors.push(...serverAuthRequiredError().errors);
37
49
  }
@@ -67,6 +79,10 @@ export async function resolveDeployAttempt(input) {
67
79
  const existing = (await input.store?.get(deploymentId)) ?? input.records.get(deploymentId);
68
80
  if (existing === undefined)
69
81
  return { deploymentId };
82
+ const versionError = deploymentRecordVersionError(existing);
83
+ if (versionError !== undefined) {
84
+ return { deploymentId, replay: { ok: false, errors: [versionError] } };
85
+ }
70
86
  if (!sameIdempotentDeploy(existing, input.tenant, input.manifest, input.options)) {
71
87
  return {
72
88
  deploymentId,
@@ -78,18 +94,23 @@ export async function resolveDeployAttempt(input) {
78
94
  },
79
95
  };
80
96
  }
81
- const ownerSubject = deploymentOwnerSubject(existing);
82
97
  return {
83
98
  deploymentId,
84
- replay: {
85
- ok: true,
86
- deploymentId: existing.deploymentId,
87
- deploymentVersion: existing.deploymentVersion,
88
- ...(existing.serverVersion !== undefined ? { serverVersion: existing.serverVersion } : {}),
89
- ...(existing.accessMode !== undefined ? { accessMode: existing.accessMode } : {}),
90
- ...(ownerSubject !== undefined ? { ownerSubject } : {}),
91
- replayed: true,
92
- },
99
+ replay: { ...deployedRecordResult(existing), replayed: true },
100
+ };
101
+ }
102
+ /** Deploy and retry responses report the committed operator policy, never the requested intent. */
103
+ export function deployedRecordResult(record) {
104
+ const ownerSubject = deploymentOwnerSubject(record);
105
+ const authentication = deploymentAuthenticationFor(record);
106
+ return {
107
+ ok: true,
108
+ deploymentId: record.deploymentId,
109
+ deploymentVersion: record.deploymentVersion,
110
+ ...(record.serverVersion !== undefined ? { serverVersion: record.serverVersion } : {}),
111
+ ...(record.accessMode !== undefined ? { accessMode: record.accessMode } : {}),
112
+ ...(authentication !== undefined ? { authentication } : {}),
113
+ ...(ownerSubject !== undefined ? { ownerSubject } : {}),
93
114
  };
94
115
  }
95
116
  function sameIdempotentDeploy(record, tenant, manifest, options) {
@@ -102,14 +123,83 @@ function sameIdempotentDeploy(record, tenant, manifest, options) {
102
123
  record.serverVersion === serverVersion &&
103
124
  record.manifest === manifest &&
104
125
  record.connectors === options.connectors &&
105
- record.accessMode === options.accessMode &&
126
+ (record.schemaVersion === 2 ||
127
+ (record.accessMode === options.accessMode &&
128
+ (options.accessMode !== 'owner-only' ||
129
+ deploymentOwnerSubject(record) === (options.ownerSubject ?? options.actor?.subject)))) &&
106
130
  record.deploymentSource === options.deploymentSource &&
107
131
  sameJson(record.hostedAssets, options.hostedAssets) &&
108
- sameJson(record.orgMembershipSources, options.orgMembershipSources) &&
109
- (options.accessMode !== 'owner-only' ||
110
- deploymentOwnerSubject(record) === (options.ownerSubject ?? options.actor?.subject)));
132
+ sameJson(record.orgMembershipSources, options.orgMembershipSources));
111
133
  }
112
134
  function sameJson(left, right) {
113
135
  return JSON.stringify(left) === JSON.stringify(right);
114
136
  }
137
+ /** Writes target one exact scope; default routing deliberately prefers a versioned deployment. */
138
+ async function activeDeploymentWriteScope(state, tenant, serverVersion) {
139
+ if (serverVersion !== undefined)
140
+ return activeRecordVersion(state, tenant, serverVersion);
141
+ // The existing store port has no unversioned point lookup. Read durable records, never a worker cache.
142
+ const records = state.store ? await state.store.loadAll() : [...state.records.values()];
143
+ return records
144
+ .filter((record) => record.active &&
145
+ record.archivedAt === undefined &&
146
+ record.serverVersion === undefined &&
147
+ sameTenantRecord(record, tenant))
148
+ .sort((a, b) => b.deploymentVersion - a.deploymentVersion)[0];
149
+ }
150
+ /** Existing dormant mixed declarations require an explicit operator adoption. */
151
+ function mixedCustomerActivationError(previous, previousAuth, accessMode, auth) {
152
+ return previous?.schemaVersion === 1 &&
153
+ previous.accessMode === 'mixed' &&
154
+ previousAuth !== undefined &&
155
+ accessMode === 'mixed' &&
156
+ auth !== undefined
157
+ ? {
158
+ code: 'customer_auth_activation_required',
159
+ path: 'accessMode',
160
+ message: 'Activate customer authentication with noodle access set mixed before redeploying this existing mixed deployment.',
161
+ }
162
+ : undefined;
163
+ }
164
+ /** Bind replacement policy to the observed active revision before the atomic store write. */
165
+ export async function replacementDeploymentPolicy(state, tenant, serverVersion, accessMode, auth, compile) {
166
+ const previous = await activeDeploymentWriteScope(state, tenant, serverVersion);
167
+ const previousBuilt = previous === undefined ? undefined : await compile(previous);
168
+ return {
169
+ previous,
170
+ schemaVersion: previous?.schemaVersion === 2 || (accessMode === 'mixed' && auth !== undefined) ? 2 : 1,
171
+ error: mixedCustomerActivationError(previous, previousBuilt?.ok ? previousBuilt.served.artifact.server.auth : previous?.serverAuth, accessMode, auth),
172
+ };
173
+ }
174
+ /** Construct one immutable deployment candidate from compiled source and resolved operator policy. */
175
+ export function createRegistryDeployRecord(input) {
176
+ const { safeTenant, safeServerVersion, deploymentId, version, built, manifest, options, schemaVersion, appPackageSnapshot, } = input;
177
+ const { actor, accessMode, automationId, deploymentSource, orgMembershipSources, connectors, hostedAssets, } = options;
178
+ const serverAuth = built.artifact.server.auth;
179
+ const ownerSubject = accessMode === 'owner-only' ? (options.ownerSubject ?? actor?.subject) : undefined;
180
+ const record = {
181
+ schemaVersion,
182
+ deploymentId,
183
+ orgSlug: safeTenant.org,
184
+ appSlug: safeTenant.app,
185
+ environment: safeTenant.env,
186
+ ...(safeServerVersion !== undefined ? { serverVersion: safeServerVersion } : {}),
187
+ deploymentVersion: version,
188
+ active: automationId === undefined,
189
+ serverName: built.artifact.server.name,
190
+ createdAt: new Date().toISOString(),
191
+ ...(actor ? { createdBySubject: actor.subject, createdByEmail: actor.email } : {}),
192
+ ...(ownerSubject !== undefined ? { ownerSubject } : {}),
193
+ ...(deploymentSource !== undefined ? { deploymentSource } : {}),
194
+ ...(accessMode !== undefined ? { accessMode } : {}),
195
+ ...(orgMembershipSources !== undefined ? { orgMembershipSources } : {}),
196
+ ...(serverAuth !== undefined ? { serverAuth } : {}),
197
+ manifest,
198
+ ...(connectors !== undefined ? { connectors } : {}),
199
+ ...(hostedAssets !== undefined && hostedAssets.length > 0 ? { hostedAssets } : {}),
200
+ ...(appPackageSnapshot !== undefined ? { appPackageSnapshot } : {}),
201
+ secrets: emptySecretEnvelope(),
202
+ };
203
+ return record;
204
+ }
115
205
  //# sourceMappingURL=registry-deploy-transaction.js.map
@@ -1,3 +1,4 @@
1
+ import { hasExactCustomerAuthProjection } from './customer-auth-audience-binding.js';
1
2
  import { withDeploymentConfiguration } from './registry-deploy-transaction.js';
2
3
  import { deploymentOwnerSubject, missingCapabilityErrors } from './registry-helpers.js';
3
4
  import { activateInMemory } from './registry-state.js';
@@ -52,9 +53,18 @@ export async function rollbackDeployment(state, ref, deploymentId, compilePersis
52
53
  };
53
54
  }
54
55
  const compiledAuth = built.served.artifact.server.auth;
56
+ if (target.serverAuth !== undefined && !hasExactCustomerAuthProjection(target, compiledAuth)) {
57
+ return {
58
+ ok: false,
59
+ status: 409,
60
+ error: 'deployment cannot be activated: server_auth_required',
61
+ };
62
+ }
55
63
  const projectedTarget = {
56
64
  ...target,
57
- ...(target.accessMode === 'customers' && compiledAuth !== undefined
65
+ ...((target.accessMode === 'customers' ||
66
+ (target.schemaVersion === 2 && target.accessMode === 'mixed')) &&
67
+ compiledAuth !== undefined
58
68
  ? { serverAuth: compiledAuth }
59
69
  : {}),
60
70
  };
@@ -75,10 +85,14 @@ export async function rollbackDeployment(state, ref, deploymentId, compilePersis
75
85
  const activation = state.store
76
86
  ? await state.store.activateDeployment(ref, deploymentId, {
77
87
  expectedAccessMode: target.accessMode,
88
+ expectedSchemaVersion: target.schemaVersion,
89
+ expectedRevision: target,
78
90
  ...(compiledAuth !== undefined ? { serverAuth: compiledAuth } : {}),
79
91
  })
80
92
  : activateInMemory(state.records, ref, deploymentId, {
81
93
  expectedAccessMode: target.accessMode,
94
+ expectedSchemaVersion: target.schemaVersion,
95
+ expectedRevision: target,
82
96
  ...(compiledAuth !== undefined ? { serverAuth: compiledAuth } : {}),
83
97
  });
84
98
  if (activation === undefined) {
@@ -1,5 +1,5 @@
1
1
  import type { ServedTarget } from '@noodle-borg/transport-http';
2
- import type { AppArchiveResult, AppRestoreResult, AppSummary, ArtifactStore, CustomerAuthRestoreProjection, DeploymentLock, DeploymentLockUpdateResult, DeploymentSummary, DeployRecord, EnvSummary, ProductionEnvironmentChange, TenantAuthConfig, TenantRef } from './store.js';
2
+ import type { AppArchiveResult, AppRestoreResult, AppSummary, ArtifactStore, CustomerAuthRestoreProjection, DeploymentActivationPrecondition, DeploymentLock, DeploymentLockUpdateResult, DeploymentPolicyPrecondition, DeploymentSummary, DeployRecord, EnvSummary, ProductionEnvironmentChange, TenantAuthConfig, TenantRef } from './store.js';
3
3
  export interface RegistryStateView {
4
4
  readonly store: ArtifactStore | undefined;
5
5
  readonly records: Map<string, DeployRecord>;
@@ -32,12 +32,9 @@ export declare function reconcilePlatformAccountResetRegistryCache(state: Regist
32
32
  * store owns deactivation; without one the caches are reconciled in-process so no instance serves a stale
33
33
  * active record.
34
34
  */
35
- export declare function persistDeployRecord(view: RegistryStateView, record: DeployRecord, tenant: TenantRef, activatesNow: boolean): Promise<DeployRecord>;
35
+ export declare function persistDeployRecord(view: RegistryStateView, record: DeployRecord, tenant: TenantRef, activatesNow: boolean, precondition?: DeploymentPolicyPrecondition): Promise<DeployRecord>;
36
36
  /** No-store activation fallback (moved verbatim from `ServerRegistry.#activateInMemory`). */
37
- export declare function activateInMemory(records: Map<string, DeployRecord>, ref: TenantRef, deploymentId: string, precondition?: {
38
- readonly expectedAccessMode: DeployRecord['accessMode'];
39
- readonly serverAuth?: DeployRecord['serverAuth'];
40
- }): {
37
+ export declare function activateInMemory(records: Map<string, DeployRecord>, ref: TenantRef, deploymentId: string, precondition?: DeploymentActivationPrecondition): {
41
38
  readonly active: DeployRecord;
42
39
  readonly previousActive?: DeployRecord;
43
40
  readonly alreadyActive: boolean;
@@ -5,8 +5,10 @@
5
5
  * durable store first, then reconcile the in-process caches so no instance keeps serving stale state.
6
6
  */
7
7
  import { normalizeServerVersion } from '@noodle-borg/module';
8
- import { assertCustomerAuthRestorePrecondition, assertUniqueActiveCustomerAuthAudienceBindings, CustomerAuthAudienceProjectionError, hasExactCustomerAuthProjection, } from './customer-auth-audience-binding.js';
8
+ import { assertCustomerAuthRestorePrecondition, assertUniqueActiveCustomerAuthAudienceBindings, CustomerAuthAudienceProjectionError, hasExactCustomerAuthProjection, requiresCustomerAuthProjection, } from './customer-auth-audience-binding.js';
9
+ import { matchesDeploymentActivation } from './deployment-activation-precondition.js';
9
10
  import { assertDeploymentActivationUnlocked, assertDeploymentAppendUnlocked, } from './deployment-lock.js';
11
+ import { assertDeploymentAppendPolicy, assertDeploymentAppendVersion, } from './deployment-record-version.js';
10
12
  import { defaultActiveRecord } from './deployment-versioning.js';
11
13
  import { recordMatchesTenant, tenantDeploymentKey, tenantKey } from './registry-targets.js';
12
14
  import { appArchivedAt, deploymentSummary, paginateAppSummaries, planAppArchive, planAppRestore, planArchivedAppSweep, resolveProductionEnvironment, summarizeApps, summarizeEnvs, withoutArchiveStamp, } from './store/records.js';
@@ -26,14 +28,16 @@ export function reconcilePlatformAccountResetRegistryCache(state, input) {
26
28
  * store owns deactivation; without one the caches are reconciled in-process so no instance serves a stale
27
29
  * active record.
28
30
  */
29
- export async function persistDeployRecord(view, record, tenant, activatesNow) {
31
+ export async function persistDeployRecord(view, record, tenant, activatesNow, precondition) {
30
32
  if (view.store) {
31
- await view.store.append(record);
33
+ await view.store.append(record, precondition);
32
34
  const persisted = (await view.store.get(record.deploymentId)) ?? record;
33
35
  view.records.set(record.deploymentId, persisted);
34
36
  return persisted;
35
37
  }
36
38
  else {
39
+ assertDeploymentAppendVersion([...view.records.values()], record);
40
+ assertDeploymentAppendPolicy([...view.records.values()], record, precondition);
37
41
  if (!assertDeploymentAppendUnlocked([...view.records.values()], record)) {
38
42
  return view.records.get(record.deploymentId);
39
43
  }
@@ -66,18 +70,22 @@ export function activateInMemory(records, ref, deploymentId, precondition) {
66
70
  if (target === undefined || !recordMatchesTenant(target, ref))
67
71
  return undefined;
68
72
  assertDeploymentActivationUnlocked([...records.values()], target);
69
- if (precondition !== undefined && target.accessMode !== precondition.expectedAccessMode) {
73
+ if (!matchesDeploymentActivation(target, precondition)) {
70
74
  return undefined;
71
75
  }
72
76
  const previousActive = [...records.values()]
73
77
  .filter((record) => record.active && recordMatchesTenant(record, ref))
74
78
  .filter((record) => record.serverVersion === target.serverVersion)
75
79
  .sort((a, b) => b.deploymentVersion - a.deploymentVersion)[0];
80
+ if (precondition === undefined && previousActive !== undefined) {
81
+ assertDeploymentAppendVersion([previousActive], { ...target, active: true });
82
+ }
76
83
  const alreadyActive = previousActive?.deploymentId === deploymentId;
77
84
  const activated = {
78
85
  ...target,
79
86
  active: true,
80
- ...(target.accessMode === 'customers' && precondition?.serverAuth !== undefined
87
+ ...(requiresCustomerAuthProjection(target, precondition?.serverAuth) &&
88
+ precondition?.serverAuth !== undefined
81
89
  ? { serverAuth: precondition.serverAuth }
82
90
  : {}),
83
91
  };
@@ -207,7 +215,7 @@ export async function registryRestoreApp(state, org, app, compileRecord) {
207
215
  return undefined;
208
216
  const projections = [];
209
217
  for (const record of plan.clear) {
210
- if (!record.active || record.accessMode !== 'customers')
218
+ if (!record.active || !requiresCustomerAuthProjection(record))
211
219
  continue;
212
220
  const built = await compileRecord(record);
213
221
  const compiledAuth = built.ok ? built.served.artifact.server.auth : undefined;
@@ -243,7 +251,7 @@ export async function registryCustomerAuthRestoreProjections(state, deploymentId
243
251
  : state.store.get(deploymentId)));
244
252
  const projections = [];
245
253
  for (const record of records) {
246
- if (record === undefined || !record.active || record.accessMode !== 'customers')
254
+ if (record === undefined || !record.active || !requiresCustomerAuthProjection(record))
247
255
  continue;
248
256
  const built = await compileRecord(record);
249
257
  const compiledAuth = built.ok ? built.served.artifact.server.auth : undefined;
@@ -1,13 +1,14 @@
1
1
  import { tenantMcpUrl } from '@noodle-borg/module';
2
+ import { hasExactCustomerAuthProjection } from './customer-auth-audience-binding.js';
3
+ import { deploymentAuthenticationFor } from './deployment-authentication.js';
2
4
  import { deploymentOwnerSubject, missingSecretNames } from './registry-helpers.js';
3
5
  /** Project one compiled deployment into the control-plane status contract. */
4
6
  export function deploymentStatusFor(ref, record, built, baseUrl, endpointOptions) {
5
7
  const missingSecrets = built.ok ? [] : missingSecretNames(built.errors);
6
- const missingCustomerAuth = built.ok &&
7
- record.accessMode === 'customers' &&
8
- built.served.artifact.server.auth === undefined;
8
+ const missingCustomerAuth = built.ok && !hasExactCustomerAuthProjection(record, built.served.artifact.server.auth);
9
9
  const unhealthy = missingCustomerAuth || (!built.ok && missingSecrets.length === 0);
10
10
  const ownerSubject = deploymentOwnerSubject(record);
11
+ const authentication = deploymentAuthenticationFor(record, built.ok ? built.served.artifact.server.auth : undefined);
11
12
  return {
12
13
  target: ref,
13
14
  deployment: {
@@ -20,6 +21,7 @@ export function deploymentStatusFor(ref, record, built, baseUrl, endpointOptions
20
21
  ...(record.createdByEmail !== undefined ? { createdByEmail: record.createdByEmail } : {}),
21
22
  ...(ownerSubject !== undefined ? { ownerSubject } : {}),
22
23
  accessMode: record.accessMode ?? 'owner-only',
24
+ ...(authentication !== undefined ? { authentication } : {}),
23
25
  ...(record.deploymentLock !== undefined
24
26
  ? {
25
27
  deploymentLock: {