@noodleseed/one 0.137.0 → 0.138.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 (123) hide show
  1. package/dist/commands/assistant-ops.d.ts.map +1 -1
  2. package/dist/commands/assistant-ops.js +30 -1
  3. package/dist/commands/assistant-ops.js.map +1 -1
  4. package/dist/commands/deploy-first-flow.d.ts +2 -0
  5. package/dist/commands/deploy-first-flow.d.ts.map +1 -1
  6. package/dist/commands/deploy-first-flow.js +46 -14
  7. package/dist/commands/deploy-first-flow.js.map +1 -1
  8. package/dist/commands/deploy-ops.d.ts.map +1 -1
  9. package/dist/commands/deploy-ops.js +27 -1
  10. package/dist/commands/deploy-ops.js.map +1 -1
  11. package/dist/commands/deploy-preflight.d.ts +1 -0
  12. package/dist/commands/deploy-preflight.d.ts.map +1 -1
  13. package/dist/commands/deploy-preflight.js +1 -0
  14. package/dist/commands/deploy-preflight.js.map +1 -1
  15. package/dist/commands/deploy-status-ops.d.ts +1 -0
  16. package/dist/commands/deploy-status-ops.d.ts.map +1 -1
  17. package/dist/commands/deploy-status-ops.js +59 -4
  18. package/dist/commands/deploy-status-ops.js.map +1 -1
  19. package/dist/commands/deployments-ops.d.ts.map +1 -1
  20. package/dist/commands/deployments-ops.js +2 -0
  21. package/dist/commands/deployments-ops.js.map +1 -1
  22. package/dist/commands/diagnostics-ops.d.ts +1 -0
  23. package/dist/commands/diagnostics-ops.d.ts.map +1 -1
  24. package/dist/commands/diagnostics-ops.js +3 -0
  25. package/dist/commands/diagnostics-ops.js.map +1 -1
  26. package/dist/deploy.d.ts +3 -0
  27. package/dist/deploy.d.ts.map +1 -1
  28. package/dist/deploy.js +4 -0
  29. package/dist/deploy.js.map +1 -1
  30. package/dist/index.d.ts +1 -1
  31. package/dist/index.d.ts.map +1 -1
  32. package/dist/index.js +1 -1
  33. package/dist/index.js.map +1 -1
  34. package/node_modules/@noodle-borg/admission-limits/dist/envelope.js +1 -1
  35. package/node_modules/@noodle-borg/agent-kit/dist/generated/example-files.js +3 -3
  36. package/node_modules/@noodle-borg/agent-kit/dist/generated/surface.js +1 -0
  37. package/node_modules/@noodle-borg/agent-kit/dist/skill-embedded-assistant-ref.js +6 -7
  38. package/node_modules/@noodle-borg/agent-kit/package.json +1 -1
  39. package/node_modules/@noodle-borg/assistant-gateway/dist/assistant-store.d.ts +6 -2
  40. package/node_modules/@noodle-borg/assistant-gateway/dist/assistant-store.js +5 -2
  41. package/node_modules/@noodle-borg/assistant-gateway/dist/model-request.js +37 -0
  42. package/node_modules/@noodle-borg/assistant-gateway/dist/model-runtime.js +4 -0
  43. package/node_modules/@noodle-borg/{service/dist/assistant-model-stream.js → assistant-gateway/dist/model-stream.js} +34 -2
  44. package/node_modules/@noodle-borg/assistant-gateway/dist/public-session.d.ts +3 -0
  45. package/node_modules/@noodle-borg/assistant-gateway/dist/public-session.js +2 -1
  46. package/node_modules/@noodle-borg/assistant-gateway/dist/public-turn.d.ts +4 -1
  47. package/node_modules/@noodle-borg/assistant-gateway/dist/public-turn.js +2 -1
  48. package/node_modules/@noodle-borg/assistant-gateway/dist/surface-budget.d.ts +6 -1
  49. package/node_modules/@noodle-borg/assistant-gateway/dist/surface-budget.js +11 -5
  50. package/node_modules/@noodle-borg/assistant-gateway/package.json +5 -1
  51. package/node_modules/@noodle-borg/authoring/dist/assistant.d.ts +10 -2
  52. package/node_modules/@noodle-borg/authoring/dist/assistant.js +4 -0
  53. package/node_modules/@noodle-borg/authoring/dist/index.d.ts +1 -1
  54. package/node_modules/@noodle-borg/authoring/dist/index.js +1 -1
  55. package/node_modules/@noodle-borg/authoring/dist/load-authored.js +1 -1
  56. package/node_modules/@noodle-borg/cli-catalog/dist/catalog-data-assistant.js +17 -0
  57. package/node_modules/@noodle-borg/cli-catalog/dist/catalog-data-hosted-deployment.js +14 -0
  58. package/node_modules/@noodle-borg/compiler/dist/artifact/types.d.ts +1 -7
  59. package/node_modules/@noodle-borg/compiler/dist/manifest/schema.d.ts +20 -8
  60. package/node_modules/@noodle-borg/compiler/dist/manifest/schema.js +12 -8
  61. package/node_modules/@noodle-borg/control-plane/dist/portable.d.ts +2 -0
  62. package/node_modules/@noodle-borg/control-plane/dist/portable.js +1 -0
  63. package/node_modules/@noodle-borg/control-plane/dist/rollback-deployment.d.ts +61 -0
  64. package/node_modules/@noodle-borg/{service/dist/operations → control-plane/dist}/rollback-deployment.js +3 -2
  65. package/node_modules/@noodle-borg/control-plane/dist/rollback-result.d.ts +19 -0
  66. package/node_modules/@noodle-borg/control-plane/package.json +3 -1
  67. package/node_modules/@noodle-borg/developer-mcp/dist/contracts.js +4 -0
  68. package/node_modules/@noodle-borg/developer-mcp/dist/widgets/app-overview.js +1 -1
  69. package/node_modules/@noodle-borg/developer-mcp/dist/widgets/deployment-detail.js +1 -1
  70. package/node_modules/@noodle-borg/module/dist/contract.d.ts +1 -0
  71. package/node_modules/@noodle-borg/module/dist/deployment-automation.d.ts +1 -1
  72. package/node_modules/@noodle-borg/module/dist/endpoint-url.d.ts +11 -0
  73. package/node_modules/@noodle-borg/module/dist/endpoint-url.js +20 -0
  74. package/node_modules/@noodle-borg/module/dist/index.d.ts +1 -0
  75. package/node_modules/@noodle-borg/module/dist/index.js +1 -0
  76. package/node_modules/@noodle-borg/observability/dist/assistant-usage.js +217 -0
  77. package/node_modules/@noodle-borg/observability/dist/index.js +2 -0
  78. package/node_modules/@noodle-borg/{service → observability}/dist/request-event-metrics.js +5 -2
  79. package/node_modules/@noodle-borg/observability/dist/request-events-postgres.js +6 -0
  80. package/node_modules/@noodle-borg/observability/dist/request-events.js +2 -0
  81. package/node_modules/@noodle-borg/service/dist/assistant-bindings.js +3 -1
  82. package/node_modules/@noodle-borg/service/dist/developer-mcp/control-plane-adapter.js +3 -3
  83. package/node_modules/@noodle-borg/service/dist/developer-mcp/inspection.js +2 -1
  84. package/node_modules/@noodle-borg/service/dist/registry-access.js +73 -24
  85. package/node_modules/@noodle-borg/service/dist/registry-deploy-transaction.js +5 -2
  86. package/node_modules/@noodle-borg/service/dist/registry-helpers.d.ts +5 -9
  87. package/node_modules/@noodle-borg/service/dist/registry-helpers.js +4 -18
  88. package/node_modules/@noodle-borg/service/dist/registry-rollback.js +3 -1
  89. package/node_modules/@noodle-borg/service/dist/registry-status.js +4 -1
  90. package/node_modules/@noodle-borg/service/dist/registry-targets.js +4 -4
  91. package/node_modules/@noodle-borg/service/dist/registry-types.d.ts +11 -16
  92. package/node_modules/@noodle-borg/service/dist/registry.d.ts +8 -5
  93. package/node_modules/@noodle-borg/service/dist/registry.js +8 -5
  94. package/node_modules/@noodle-borg/service/dist/routes/access.js +19 -1
  95. package/node_modules/@noodle-borg/service/dist/routes/analytics-dispatch.js +17 -1
  96. package/node_modules/@noodle-borg/service/dist/routes/analytics.js +1 -1
  97. package/node_modules/@noodle-borg/service/dist/routes/assistant-agent.js +73 -59
  98. package/node_modules/@noodle-borg/service/dist/routes/assistant-doctor.js +1 -1
  99. package/node_modules/@noodle-borg/service/dist/routes/assistant-model-binding.js +28 -0
  100. package/node_modules/@noodle-borg/service/dist/routes/assistant-public-session.js +18 -0
  101. package/node_modules/@noodle-borg/service/dist/routes/assistant-public-turn.js +6 -0
  102. package/node_modules/@noodle-borg/service/dist/routes/assistant-usage.js +42 -0
  103. package/node_modules/@noodle-borg/service/dist/routes/assistant.js +20 -3
  104. package/node_modules/@noodle-borg/service/dist/routes/auth-doctor-live.js +1 -2
  105. package/node_modules/@noodle-borg/service/dist/routes/control-plane.js +49 -4
  106. package/node_modules/@noodle-borg/service/dist/routes/deploy-preflight.js +11 -1
  107. package/node_modules/@noodle-borg/service/dist/routes/endpoint-enrichment.js +2 -1
  108. package/node_modules/@noodle-borg/service/dist/routes/paths.js +4 -0
  109. package/node_modules/@noodle-borg/service/dist/routes/rollback.js +1 -1
  110. package/node_modules/@noodle-borg/service/dist/service.js +6 -0
  111. package/node_modules/@noodle-borg/service/dist/store/in-memory.d.ts +2 -5
  112. package/node_modules/@noodle-borg/service/dist/store/in-memory.js +9 -21
  113. package/node_modules/@noodle-borg/service/dist/store/json-file.d.ts +2 -5
  114. package/node_modules/@noodle-borg/service/dist/store/json-file.js +8 -20
  115. package/node_modules/@noodle-borg/service/dist/store/records.js +39 -0
  116. package/node_modules/@noodle-borg/service/dist/store.d.ts +21 -10
  117. package/node_modules/@noodle-borg/service/package.json +5 -1
  118. package/node_modules/@noodle-borg/wire-contracts/dist/control-plane.d.ts +120 -0
  119. package/node_modules/@noodle-borg/wire-contracts/dist/control-plane.js +123 -1
  120. package/node_modules/@noodle-borg/wire-contracts/dist/index.d.ts +4 -0
  121. package/node_modules/@noodle-borg/wire-contracts/dist/index.js +15 -1
  122. package/node_modules/@noodleseed/assistant/package.json +1 -1
  123. package/package.json +2 -2
@@ -3,7 +3,7 @@ import { deployPreflightRequestSchema, formatWireError } from '@noodle-borg/wire
3
3
  import { readDeployBody, sendJson } from '../http-util.js';
4
4
  import { manifestUsesUserRoot } from '../registry-access.js';
5
5
  import { isIdentityAccessMode } from './access-mode.js';
6
- import { authorizeTenantControl, developerGrantRouteAccess } from './control-plane.js';
6
+ import { authorizeDeploymentOwnerSelection, authorizeTenantControl, developerGrantRouteAccess, } from './control-plane.js';
7
7
  /**
8
8
  * Read-only first-deploy readiness. It intentionally does not verify uploaded asset bytes:
9
9
  * the CLI calls this before it requests upload targets or sends any bytes.
@@ -49,6 +49,9 @@ export async function handleDeployPreflight(req, res, registry, options, maxBody
49
49
  error: `${accessMode} deployments require an authenticated deployer`,
50
50
  });
51
51
  }
52
+ const ownerSelection = await authorizeDeploymentOwnerSelection(res, controlPlane, tenant.org, accessMode, identity, parsed.ownerSubject);
53
+ if (!ownerSelection.ok)
54
+ return;
52
55
  if (accessMode === 'public' && manifestUsesUserRoot(parsed.manifest)) {
53
56
  return sendJson(res, 400, {
54
57
  code: 'public_user_context_conflict',
@@ -71,6 +74,7 @@ export async function handleDeployPreflight(req, res, registry, options, maxBody
71
74
  ...(parsed.connectors !== undefined ? { connectors: parsed.connectors } : {}),
72
75
  actor: identity,
73
76
  accessMode,
77
+ ownerSubject: ownerSelection.ownerSubject,
74
78
  ...(parsed.orgMembershipSources !== undefined
75
79
  ? { orgMembershipSources: parsed.orgMembershipSources }
76
80
  : {}),
@@ -99,6 +103,9 @@ export async function handleDeployPreflight(req, res, registry, options, maxBody
99
103
  ready,
100
104
  appState,
101
105
  environmentState,
106
+ ...(ownerSelection.ownerSubject !== undefined
107
+ ? { ownerSubject: ownerSelection.ownerSubject }
108
+ : {}),
102
109
  errorCount: errors.length,
103
110
  codes,
104
111
  missingSecretCount: missingSecrets.length,
@@ -108,6 +115,9 @@ export async function handleDeployPreflight(req, res, registry, options, maxBody
108
115
  return sendJson(res, 200, {
109
116
  ok: true,
110
117
  ready,
118
+ ...(ownerSelection.ownerSubject !== undefined
119
+ ? { ownerSubject: ownerSelection.ownerSubject }
120
+ : {}),
111
121
  target: {
112
122
  ...tenant,
113
123
  appState,
@@ -1,4 +1,5 @@
1
- import { tenantMcpUrl } from '../registry-helpers.js';
1
+ import { tenantMcpUrl } from '@noodle-borg/module';
2
+ export { tenantMcpUrl };
2
3
  /** Stamp `endpointUrl` onto one deployment summary. */
3
4
  export function withEndpointUrl(summary, base, options = {}) {
4
5
  return {
@@ -204,6 +204,10 @@ export function parseTenantSessionPath(pathname) {
204
204
  sessionId: decodeSegment(match, 4),
205
205
  }));
206
206
  }
207
+ /** `GET .../assistant/usage` — scalar-only embedded assistant engagement and model usage. */
208
+ export function parseTenantAssistantUsagePath(pathname) {
209
+ return matchPath(pathname, /^\/v1\/orgs\/([^/]+)\/apps\/([^/]+)\/envs\/([^/]+)\/assistant\/usage$/, tenantRef);
210
+ }
207
211
  /** Match `/v1/orgs/{org}/apps/{app}/github/connection` (GHD-1 repository connection, GET/POST/DELETE). */
208
212
  export function parseGithubConnectionPath(pathname) {
209
213
  return matchPath(pathname, /^\/v1\/orgs\/([^/]+)\/apps\/([^/]+)\/github\/connection$/, appRef);
@@ -1,6 +1,6 @@
1
+ import { rollbackDeploymentOperation } from '@noodle-borg/control-plane/portable';
1
2
  import { baseFromRequest, readJsonBody, sendForbidden, sendJson } from '../http-util.js';
2
3
  import { endpointUrlOptionsForOrg } from '../mcp-public-routing.js';
3
- import { rollbackDeploymentOperation } from '../operations/rollback-deployment.js';
4
4
  import { authorizeTenantControl, developerGrantRouteAccess } from './control-plane.js';
5
5
  export async function handleRollback(req, res, registry, gate, controlPlane, audit, maxBody, tenant, options) {
6
6
  const identity = await authorizeTenantControl(req, res, gate, controlPlane, tenant.org, developerGrantRouteAccess(options.developerGrantStore, 'deployments:rollback'));
@@ -253,6 +253,12 @@ export function createServiceHandler(registry, options = {}) {
253
253
  ...(options.assistantModelFetch !== undefined
254
254
  ? { modelFetch: options.assistantModelFetch }
255
255
  : {}),
256
+ ...(options.managedAssistantModelResolver !== undefined
257
+ ? { managedModelResolver: options.managedAssistantModelResolver }
258
+ : {}),
259
+ ...(options.captureRequestEvent !== undefined
260
+ ? { captureRequestEvent: options.captureRequestEvent }
261
+ : {}),
256
262
  ...(options.clock !== undefined ? { clock: options.clock } : {}),
257
263
  logger,
258
264
  applySecurityHeaders,
@@ -1,5 +1,5 @@
1
1
  import type { AccessMode } from '@noodle-borg/transport-http';
2
- import type { AppArchiveResult, AppRestorePrecondition, AppRestoreResult, AppSummary, ArtifactStore, DeploymentActivationResult, DeploymentListFilter, DeploymentLock, DeploymentLockUpdateResult, DeploymentSummary, DeployRecord, EnvSummary, ProductionEnvironmentChange, TenantAuthConfig, TenantRef } from '../store.js';
2
+ import type { ActiveAccessUpdateInput, AppArchiveResult, AppRestorePrecondition, AppRestoreResult, AppSummary, ArtifactStore, DeploymentActivationResult, DeploymentListFilter, DeploymentLock, DeploymentLockUpdateResult, DeploymentSummary, DeployRecord, EnvSummary, ProductionEnvironmentChange, TenantAuthConfig, TenantRef } from '../store.js';
3
3
  /** In-memory store: keeps records in a Map. Used by tests and as an explicit, non-persistent option. */
4
4
  export declare class InMemoryArtifactStore implements ArtifactStore {
5
5
  #private;
@@ -16,10 +16,7 @@ export declare class InMemoryArtifactStore implements ArtifactStore {
16
16
  }, _options?: {
17
17
  readonly automationId?: string;
18
18
  }): Promise<DeploymentActivationResult | undefined>;
19
- updateActiveAccess(ref: TenantRef, deploymentId: string, accessMode: AccessMode, precondition: {
20
- readonly expectedAccessMode: AccessMode | undefined;
21
- readonly serverAuth?: TenantAuthConfig;
22
- }): Promise<DeployRecord | undefined>;
19
+ updateActiveAccess(ref: TenantRef, deploymentId: string, input: ActiveAccessUpdateInput): Promise<DeployRecord | undefined>;
23
20
  listDeployments(filter: DeploymentListFilter): Promise<readonly DeploymentSummary[]>;
24
21
  getAppArchivedAt(org: string, app: string): Promise<string | undefined>;
25
22
  archiveApp(org: string, app: string, at: string): Promise<AppArchiveResult | undefined>;
@@ -3,7 +3,7 @@ import { assertSameAppPackageSnapshot, sanitizeDeployRecordAppPackageSnapshot, }
3
3
  import { assertCustomerAuthRestorePrecondition, assertUniqueActiveCustomerAuthAudienceBindings, findActiveCustomerAuthAudienceConflict, } from '../customer-auth-audience-binding.js';
4
4
  import { assertDeploymentActivationUnlocked, assertDeploymentAppendUnlocked, } from '../deployment-lock.js';
5
5
  import { defaultActiveRecord, sameDeploymentScope, sameTenantRecord, } from '../deployment-versioning.js';
6
- import { appArchivedAt, deploymentSummary, matchesDeploymentFilter, paginateAppSummaries, planAppArchive, planAppRestore, resolveProductionEnvironment, summarizeApps, summarizeEnvs, validateDeploymentListFilter, withoutArchiveStamp, } from './records.js';
6
+ import { appArchivedAt, deploymentSummary, matchesDeploymentFilter, paginateAppSummaries, planAppArchive, planAppRestore, preserveDeploymentOwnerState, resolveActiveAccessUpdate, resolveProductionEnvironment, summarizeApps, summarizeEnvs, validateDeploymentListFilter, withoutArchiveStamp, } from './records.js';
7
7
  import { validateSlug, validateTenantRef } from './validate.js';
8
8
  /** In-memory store: keeps records in a Map. Used by tests and as an explicit, non-persistent option. */
9
9
  export class InMemoryArtifactStore {
@@ -14,6 +14,8 @@ export class InMemoryArtifactStore {
14
14
  if (existing !== undefined)
15
15
  assertSameAppPackageSnapshot(existing, record);
16
16
  record = sanitizeDeployRecordAppPackageSnapshot(record);
17
+ if (existing !== undefined)
18
+ record = preserveDeploymentOwnerState(existing, record);
17
19
  validateTenantRef({ org: record.orgSlug, app: record.appSlug, env: record.environment });
18
20
  if (!assertDeploymentAppendUnlocked([...this.#records.values()], record))
19
21
  return;
@@ -117,29 +119,15 @@ export class InMemoryArtifactStore {
117
119
  }
118
120
  }
119
121
  }
120
- async updateActiveAccess(ref, deploymentId, accessMode, precondition) {
122
+ async updateActiveAccess(ref, deploymentId, input) {
121
123
  const safe = validateTenantRef(ref);
122
124
  const record = this.#records.get(deploymentId);
123
- if (record === undefined ||
124
- !record.active ||
125
- record.archivedAt !== undefined ||
126
- !sameTenantRecord(record, safe) ||
127
- record.accessMode !== precondition.expectedAccessMode) {
128
- return Promise.resolve(undefined);
129
- }
130
- if ((record.accessMode ?? 'owner-only') === accessMode && accessMode !== 'customers') {
131
- return Promise.resolve(record);
132
- }
133
- const updated = {
134
- ...record,
135
- accessMode,
136
- ...(accessMode === 'customers' && precondition.serverAuth !== undefined
137
- ? { serverAuth: precondition.serverAuth }
138
- : {}),
139
- };
140
- const proposed = new Map(this.#records).set(record.deploymentId, updated);
125
+ const updated = resolveActiveAccessUpdate(record, safe, input);
126
+ if (updated === undefined || updated === record)
127
+ return Promise.resolve(updated);
128
+ const proposed = new Map(this.#records).set(updated.deploymentId, updated);
141
129
  assertUniqueActiveCustomerAuthAudienceBindings([...proposed.values()]);
142
- this.#records.set(record.deploymentId, updated);
130
+ this.#records.set(updated.deploymentId, updated);
143
131
  return Promise.resolve(updated);
144
132
  }
145
133
  listDeployments(filter) {
@@ -1,5 +1,5 @@
1
1
  import type { AccessMode } from '@noodle-borg/transport-http';
2
- import type { AppArchiveResult, AppRestorePrecondition, AppRestoreResult, AppSummary, ArtifactStore, DeploymentActivationResult, DeploymentListFilter, DeploymentLock, DeploymentLockUpdateResult, DeploymentSummary, DeployRecord, EnvSummary, ProductionEnvironmentChange, TenantAuthConfig, TenantRef } from '../store.js';
2
+ import type { ActiveAccessUpdateInput, AppArchiveResult, AppRestorePrecondition, AppRestoreResult, AppSummary, ArtifactStore, DeploymentActivationResult, DeploymentListFilter, DeploymentLock, DeploymentLockUpdateResult, DeploymentSummary, DeployRecord, EnvSummary, ProductionEnvironmentChange, TenantAuthConfig, TenantRef } from '../store.js';
3
3
  /**
4
4
  * JSON-file store: one file per deployment at `<dataDir>/deployments/<deploymentId>.json`, written atomically
5
5
  * (write a temp file, then `rename` over the target — no partial file is ever read). `deploymentId` is a
@@ -15,10 +15,7 @@ export declare class JsonFileArtifactStore implements ArtifactStore {
15
15
  getActiveByTenantVersion(ref: TenantRef, serverVersion: string): Promise<DeployRecord | undefined>;
16
16
  setDeploymentLock(ref: TenantRef, serverVersion: string, expectedDeploymentId: string, deploymentLock: DeploymentLock | undefined): Promise<DeploymentLockUpdateResult>;
17
17
  findActiveCustomerAuthAudienceConflict(ref: TenantRef, auth: TenantAuthConfig): Promise<TenantRef | undefined>;
18
- updateActiveAccess(ref: TenantRef, deploymentId: string, accessMode: AccessMode, precondition: {
19
- readonly expectedAccessMode: AccessMode | undefined;
20
- readonly serverAuth?: TenantAuthConfig;
21
- }): Promise<DeployRecord | undefined>;
18
+ updateActiveAccess(ref: TenantRef, deploymentId: string, input: ActiveAccessUpdateInput): Promise<DeployRecord | undefined>;
22
19
  activateDeployment(ref: TenantRef, deploymentId: string, precondition?: {
23
20
  readonly expectedAccessMode: AccessMode | undefined;
24
21
  readonly serverAuth?: TenantAuthConfig;
@@ -6,7 +6,7 @@ import { assertSameAppPackageSnapshot, sanitizeDeployRecordAppPackageSnapshot, }
6
6
  import { assertCustomerAuthRestorePrecondition, assertUniqueActiveCustomerAuthAudienceBindings, findActiveCustomerAuthAudienceConflict, } from '../customer-auth-audience-binding.js';
7
7
  import { assertDeploymentActivationUnlocked, assertDeploymentAppendUnlocked, } from '../deployment-lock.js';
8
8
  import { defaultActiveRecord, sameDeploymentScope, sameTenantRecord, } from '../deployment-versioning.js';
9
- import { appArchivedAt, deploymentSummary, matchesDeploymentFilter, paginateAppSummaries, planAppArchive, planAppRestore, resolveProductionEnvironment, summarizeApps, summarizeEnvs, validateDeploymentListFilter, withoutArchiveStamp, } from './records.js';
9
+ import { appArchivedAt, deploymentSummary, matchesDeploymentFilter, paginateAppSummaries, planAppArchive, planAppRestore, preserveDeploymentOwnerState, resolveActiveAccessUpdate, resolveProductionEnvironment, summarizeApps, summarizeEnvs, validateDeploymentListFilter, withoutArchiveStamp, } from './records.js';
10
10
  import { DEPLOYMENT_ID_PATTERN, validateSlug, validateTenantRef } from './validate.js';
11
11
  /**
12
12
  * JSON-file store: one file per deployment at `<dataDir>/deployments/<deploymentId>.json`, written atomically
@@ -38,6 +38,8 @@ export class JsonFileArtifactStore {
38
38
  if (existing !== undefined)
39
39
  assertSameAppPackageSnapshot(existing, record);
40
40
  record = sanitizeDeployRecordAppPackageSnapshot(record);
41
+ if (existing !== undefined)
42
+ record = preserveDeploymentOwnerState(existing, record);
41
43
  if (!assertDeploymentAppendUnlocked(records, record))
42
44
  return;
43
45
  const proposed = new Map(records.map((item) => [item.deploymentId, item]));
@@ -104,28 +106,14 @@ export class JsonFileArtifactStore {
104
106
  async findActiveCustomerAuthAudienceConflict(ref, auth) {
105
107
  return findActiveCustomerAuthAudienceConflict(await this.loadAll(), validateTenantRef(ref), auth);
106
108
  }
107
- async updateActiveAccess(ref, deploymentId, accessMode, precondition) {
109
+ async updateActiveAccess(ref, deploymentId, input) {
108
110
  const safe = validateTenantRef(ref);
109
111
  return this.#serializeLifecycle(async () => {
110
112
  const record = await this.get(deploymentId);
111
- if (record === undefined ||
112
- !record.active ||
113
- record.archivedAt !== undefined ||
114
- !sameTenantRecord(record, safe) ||
115
- record.accessMode !== precondition.expectedAccessMode) {
116
- return undefined;
117
- }
118
- if ((record.accessMode ?? 'owner-only') === accessMode && accessMode !== 'customers') {
119
- return record;
120
- }
121
- const updated = {
122
- ...record,
123
- accessMode,
124
- ...(accessMode === 'customers' && precondition.serverAuth !== undefined
125
- ? { serverAuth: precondition.serverAuth }
126
- : {}),
127
- };
128
- const proposed = new Map((await this.loadAll()).map((item) => [item.deploymentId, item])).set(record.deploymentId, updated);
113
+ const updated = resolveActiveAccessUpdate(record, safe, input);
114
+ if (updated === undefined || updated === record)
115
+ return updated;
116
+ const proposed = new Map((await this.loadAll()).map((item) => [item.deploymentId, item])).set(updated.deploymentId, updated);
129
117
  assertUniqueActiveCustomerAuthAudienceBindings([...proposed.values()]);
130
118
  await this.#writeRecord(updated);
131
119
  return updated;
@@ -1,6 +1,16 @@
1
+ /**
2
+ * Pure record-level helpers shared by every {@link ArtifactStore} backend: the canonical
3
+ * deployment-summary mapping, list filtering, app archive/restore planning (ADR 0117), and the
4
+ * `apps` resource aggregation (`summarizeApps`). No I/O here — backends apply the plans to their
5
+ * own persistence and feed their own raw records/anchors through these pure functions so every
6
+ * backend groups, ranks, and sorts identically.
7
+ */
8
+ import { sameTenantRecord } from '../deployment-versioning.js';
9
+ import { deploymentOwnerSubject } from '../registry-helpers.js';
1
10
  import { validateSlug } from './validate.js';
2
11
  /** The canonical, secret-free list projection of a {@link DeployRecord}. */
3
12
  export function deploymentSummary(record) {
13
+ const ownerSubject = deploymentOwnerSubject(record);
4
14
  return {
5
15
  deploymentId: record.deploymentId,
6
16
  orgSlug: record.orgSlug,
@@ -11,6 +21,7 @@ export function deploymentSummary(record) {
11
21
  serverName: record.serverName,
12
22
  createdAt: record.createdAt,
13
23
  ...(record.createdByEmail !== undefined ? { createdByEmail: record.createdByEmail } : {}),
24
+ ...(ownerSubject !== undefined ? { ownerSubject } : {}),
14
25
  ...(record.deploymentSource !== undefined ? { deploymentSource: record.deploymentSource } : {}),
15
26
  accessMode: record.accessMode ?? 'owner-only',
16
27
  ...(record.deploymentLock !== undefined
@@ -95,6 +106,34 @@ export function withoutArchiveStamp(record) {
95
106
  const { archivedAt: _cleared, ...rest } = record;
96
107
  return rest;
97
108
  }
109
+ export function preserveDeploymentOwnerState(existing, candidate) {
110
+ const { ownerSubject: _candidateOwnerSubject, ...deployFields } = candidate;
111
+ return {
112
+ ...deployFields,
113
+ ...(existing.ownerSubject !== undefined ? { ownerSubject: existing.ownerSubject } : {}),
114
+ };
115
+ }
116
+ export function resolveActiveAccessUpdate(record, ref, input) {
117
+ if (record === undefined ||
118
+ !record.active ||
119
+ record.archivedAt !== undefined ||
120
+ !sameTenantRecord(record, ref) ||
121
+ record.accessMode !== input.expectedAccessMode ||
122
+ deploymentOwnerSubject(record) !== input.expectedOwnerSubject)
123
+ return undefined;
124
+ if ((record.accessMode ?? 'owner-only') === input.accessMode &&
125
+ (input.ownerSubject === undefined || deploymentOwnerSubject(record) === input.ownerSubject) &&
126
+ input.accessMode !== 'customers')
127
+ return record;
128
+ return {
129
+ ...record,
130
+ accessMode: input.accessMode,
131
+ ...(input.ownerSubject !== undefined ? { ownerSubject: input.ownerSubject } : {}),
132
+ ...(input.accessMode === 'customers' && input.serverAuth !== undefined
133
+ ? { serverAuth: input.serverAuth }
134
+ : {}),
135
+ };
136
+ }
98
137
  const ENVIRONMENT_RANK = { prod: 0, staging: 1, dev: 2 };
99
138
  function environmentRank(environment) {
100
139
  return ENVIRONMENT_RANK[environment] ?? 3;
@@ -47,6 +47,8 @@ export interface DeployRecord {
47
47
  readonly createdAt: string;
48
48
  readonly createdBySubject?: string;
49
49
  readonly createdByEmail?: string;
50
+ /** OAuth subject authorized by owner-only access; absent legacy records fall back to the creator. */
51
+ readonly ownerSubject?: string;
50
52
  /** Informational deploy origin. Absent on records written before source tracking. */
51
53
  readonly deploymentSource?: DeploymentSource;
52
54
  /**
@@ -142,6 +144,7 @@ export interface DeploymentSummary {
142
144
  readonly serverName: string;
143
145
  readonly createdAt: string;
144
146
  readonly createdByEmail?: string;
147
+ readonly ownerSubject?: string;
145
148
  readonly deploymentSource?: DeploymentSource;
146
149
  readonly accessMode: AccessMode;
147
150
  readonly deploymentLock?: DeploymentLockMetadata;
@@ -236,6 +239,7 @@ export interface DeploymentStatus {
236
239
  readonly serverName: string;
237
240
  readonly createdAt: string;
238
241
  readonly createdByEmail?: string;
242
+ readonly ownerSubject?: string;
239
243
  readonly accessMode: AccessMode;
240
244
  readonly deploymentLock?: DeploymentLockMetadata;
241
245
  };
@@ -252,6 +256,16 @@ export interface DeploymentActivationResult {
252
256
  readonly previousActive?: DeployRecord;
253
257
  readonly alreadyActive: boolean;
254
258
  }
259
+ /** One atomic access/owner compare-and-set over the active deployment's observed state. */
260
+ export interface ActiveAccessUpdateInput {
261
+ readonly accessMode: AccessMode;
262
+ /** Desired owner mutation. Omission preserves the current effective owner; clearing is unsupported. */
263
+ readonly ownerSubject?: string;
264
+ readonly expectedAccessMode: AccessMode | undefined;
265
+ readonly expectedOwnerSubject: string | undefined;
266
+ /** Compiler-authoritative projection persisted atomically when enabling customer access. */
267
+ readonly serverAuth?: TenantAuthConfig;
268
+ }
255
269
  export type DeploymentLockUpdateResult = {
256
270
  readonly ok: true;
257
271
  readonly record: DeployRecord;
@@ -263,9 +277,10 @@ export type DeploymentLockUpdateResult = {
263
277
  export interface ControlPlaneStore extends OrganizationStore {
264
278
  }
265
279
  /**
266
- * Durable storage for deployed-server records. `append` writes (or replaces) one server's record;
267
- * `loadAll` returns every current record for restart recovery. Historical records are retained so a
268
- * tenant environment can reactivate an earlier deployment without changing the tenant-facing URL.
280
+ * Durable storage for deployed-server records. `append` writes (or replaces) one server's immutable deploy
281
+ * inputs while preserving owner state changed through {@link updateActiveAccess}; `loadAll` returns every
282
+ * current record for restart recovery. Historical records are retained so a tenant environment can reactivate
283
+ * an earlier deployment without changing the tenant-facing URL.
269
284
  */
270
285
  export interface ArtifactStore {
271
286
  append(record: DeployRecord): Promise<void>;
@@ -294,14 +309,10 @@ export interface ArtifactStore {
294
309
  readonly automationId?: string;
295
310
  }): Promise<DeploymentActivationResult | undefined>;
296
311
  /**
297
- * Compare and set the active record's access mode. The precondition is the raw persisted value:
298
- * legacy `undefined` remains distinguishable from an explicitly stored `owner-only`.
312
+ * Compare and set the active record's access mode and effective owner. The access precondition is the raw
313
+ * persisted value; the owner precondition resolves legacy records through creator provenance.
299
314
  */
300
- updateActiveAccess(ref: TenantRef, deploymentId: string, accessMode: AccessMode, precondition: {
301
- readonly expectedAccessMode: AccessMode | undefined;
302
- /** Compiler-authoritative projection persisted atomically when enabling customer access. */
303
- readonly serverAuth?: TenantAuthConfig;
304
- }): Promise<DeployRecord | undefined>;
315
+ updateActiveAccess(ref: TenantRef, deploymentId: string, input: ActiveAccessUpdateInput): Promise<DeployRecord | undefined>;
305
316
  listDeployments(filter: DeploymentListFilter): Promise<readonly DeploymentSummary[]>;
306
317
  /**
307
318
  * App soft delete (ADR 0117). `getAppArchivedAt` reports the app's archived stamp (defined iff the
@@ -20,6 +20,10 @@
20
20
  "./main": {
21
21
  "types": "./dist/main.d.ts",
22
22
  "import": "./dist/main.js"
23
+ },
24
+ "./oauth-google": {
25
+ "types": "./dist/oauth/google.d.ts",
26
+ "import": "./dist/oauth/google.js"
23
27
  }
24
28
  },
25
29
  "bin": {
@@ -35,7 +39,7 @@
35
39
  "dependencies": {
36
40
  "@modelcontextprotocol/sdk": "^1.29.0",
37
41
  "@noodle-borg/admission-limits": "0.0.0",
38
- "@noodle-borg/agent-kit": "0.81.0",
42
+ "@noodle-borg/agent-kit": "0.82.0",
39
43
  "@noodle-borg/app-package": "0.0.0",
40
44
  "@noodle-borg/assistant-gateway": "0.0.0",
41
45
  "@noodle-borg/auth": "0.0.0",
@@ -1,5 +1,7 @@
1
1
  /** Strict service-output contracts for control-plane reads and mutations (ADR 0128). */
2
2
  import { z } from 'zod';
3
+ /** Exact OAuth subject bound by an operator to an owner-only deployment. */
4
+ export declare const deploymentOwnerSubjectSchema: z.ZodString;
3
5
  export declare const accessUpdateRequestSchema: z.ZodObject<{
4
6
  accessMode: z.ZodEnum<{
5
7
  "owner-only": "owner-only";
@@ -9,8 +11,118 @@ export declare const accessUpdateRequestSchema: z.ZodObject<{
9
11
  mixed: "mixed";
10
12
  customers: "customers";
11
13
  }>;
14
+ ownerSubject: z.ZodOptional<z.ZodString>;
12
15
  serverVersion: z.ZodOptional<z.ZodString>;
13
16
  }, z.core.$strict>;
17
+ export declare const accessUpdateResponseSchema: z.ZodObject<{
18
+ ok: z.ZodLiteral<true>;
19
+ target: z.ZodObject<{
20
+ org: z.ZodString;
21
+ app: z.ZodString;
22
+ env: z.ZodString;
23
+ }, z.core.$strict>;
24
+ deployment: z.ZodObject<{
25
+ deploymentId: z.ZodString;
26
+ serverVersion: z.ZodOptional<z.ZodString>;
27
+ accessMode: z.ZodEnum<{
28
+ "owner-only": "owner-only";
29
+ "org-members": "org-members";
30
+ authenticated: "authenticated";
31
+ public: "public";
32
+ mixed: "mixed";
33
+ customers: "customers";
34
+ }>;
35
+ ownerSubject: z.ZodOptional<z.ZodString>;
36
+ }, z.core.$strict>;
37
+ previousAccessMode: z.ZodEnum<{
38
+ "owner-only": "owner-only";
39
+ "org-members": "org-members";
40
+ authenticated: "authenticated";
41
+ public: "public";
42
+ mixed: "mixed";
43
+ customers: "customers";
44
+ }>;
45
+ previousOwnerSubject: z.ZodOptional<z.ZodString>;
46
+ accessChanged: z.ZodBoolean;
47
+ ownerChanged: z.ZodBoolean;
48
+ changed: z.ZodBoolean;
49
+ }, z.core.$strict>;
50
+ export type AccessUpdateResponse = z.output<typeof accessUpdateResponseSchema>;
51
+ /**
52
+ * Additive client reader for the access mutation response. Older services emitted only the aggregate
53
+ * `changed` bit, so the mode component is derived from the authoritative previous/current modes while an
54
+ * unobservable owner component remains omitted. Modern component bits retain the strict cross-field check.
55
+ */
56
+ export declare const accessUpdateClientResponseSchema: z.ZodPipe<z.ZodObject<{
57
+ ok: z.ZodLiteral<true>;
58
+ target: z.ZodObject<{
59
+ org: z.ZodString;
60
+ app: z.ZodString;
61
+ env: z.ZodString;
62
+ }, z.core.$strip>;
63
+ deployment: z.ZodObject<{
64
+ deploymentId: z.ZodString;
65
+ serverVersion: z.ZodOptional<z.ZodString>;
66
+ accessMode: z.ZodEnum<{
67
+ "owner-only": "owner-only";
68
+ "org-members": "org-members";
69
+ authenticated: "authenticated";
70
+ public: "public";
71
+ mixed: "mixed";
72
+ customers: "customers";
73
+ }>;
74
+ ownerSubject: z.ZodOptional<z.ZodString>;
75
+ }, z.core.$strip>;
76
+ previousAccessMode: z.ZodEnum<{
77
+ "owner-only": "owner-only";
78
+ "org-members": "org-members";
79
+ authenticated: "authenticated";
80
+ public: "public";
81
+ mixed: "mixed";
82
+ customers: "customers";
83
+ }>;
84
+ previousOwnerSubject: z.ZodOptional<z.ZodString>;
85
+ accessChanged: z.ZodOptional<z.ZodBoolean>;
86
+ ownerChanged: z.ZodOptional<z.ZodBoolean>;
87
+ changed: z.ZodBoolean;
88
+ }, z.core.$strip>, z.ZodTransform<{
89
+ accessChanged: boolean;
90
+ ok: true;
91
+ target: {
92
+ org: string;
93
+ app: string;
94
+ env: string;
95
+ };
96
+ deployment: {
97
+ deploymentId: string;
98
+ accessMode: "owner-only" | "org-members" | "authenticated" | "public" | "mixed" | "customers";
99
+ serverVersion?: string | undefined;
100
+ ownerSubject?: string | undefined;
101
+ };
102
+ previousAccessMode: "owner-only" | "org-members" | "authenticated" | "public" | "mixed" | "customers";
103
+ changed: boolean;
104
+ previousOwnerSubject?: string | undefined;
105
+ ownerChanged?: boolean | undefined;
106
+ }, {
107
+ ok: true;
108
+ target: {
109
+ org: string;
110
+ app: string;
111
+ env: string;
112
+ };
113
+ deployment: {
114
+ deploymentId: string;
115
+ accessMode: "owner-only" | "org-members" | "authenticated" | "public" | "mixed" | "customers";
116
+ serverVersion?: string | undefined;
117
+ ownerSubject?: string | undefined;
118
+ };
119
+ previousAccessMode: "owner-only" | "org-members" | "authenticated" | "public" | "mixed" | "customers";
120
+ changed: boolean;
121
+ previousOwnerSubject?: string | undefined;
122
+ accessChanged?: boolean | undefined;
123
+ ownerChanged?: boolean | undefined;
124
+ }>>;
125
+ export type AccessUpdateClientResponse = z.output<typeof accessUpdateClientResponseSchema>;
14
126
  export declare const deploymentLockRequestSchema: z.ZodObject<{
15
127
  serverVersion: z.ZodString;
16
128
  expectedDeploymentId: z.ZodString;
@@ -27,6 +139,7 @@ export declare const DeploymentSummarySchema: z.ZodObject<{
27
139
  serverName: z.ZodString;
28
140
  createdAt: z.ZodString;
29
141
  createdByEmail: z.ZodOptional<z.ZodString>;
142
+ ownerSubject: z.ZodOptional<z.ZodString>;
30
143
  deploymentSource: z.ZodOptional<z.ZodString>;
31
144
  accessMode: z.ZodEnum<{
32
145
  "owner-only": "owner-only";
@@ -66,6 +179,7 @@ export declare const AppSummarySchema: z.ZodObject<{
66
179
  serverName: z.ZodString;
67
180
  createdAt: z.ZodString;
68
181
  createdByEmail: z.ZodOptional<z.ZodString>;
182
+ ownerSubject: z.ZodOptional<z.ZodString>;
69
183
  deploymentSource: z.ZodOptional<z.ZodString>;
70
184
  accessMode: z.ZodEnum<{
71
185
  "owner-only": "owner-only";
@@ -107,6 +221,7 @@ export declare const AppsListResponseSchema: z.ZodObject<{
107
221
  serverName: z.ZodString;
108
222
  createdAt: z.ZodString;
109
223
  createdByEmail: z.ZodOptional<z.ZodString>;
224
+ ownerSubject: z.ZodOptional<z.ZodString>;
110
225
  deploymentSource: z.ZodOptional<z.ZodString>;
111
226
  accessMode: z.ZodEnum<{
112
227
  "owner-only": "owner-only";
@@ -150,6 +265,7 @@ export declare const AppResponseSchema: z.ZodObject<{
150
265
  serverName: z.ZodString;
151
266
  createdAt: z.ZodString;
152
267
  createdByEmail: z.ZodOptional<z.ZodString>;
268
+ ownerSubject: z.ZodOptional<z.ZodString>;
153
269
  deploymentSource: z.ZodOptional<z.ZodString>;
154
270
  accessMode: z.ZodEnum<{
155
271
  "owner-only": "owner-only";
@@ -190,6 +306,7 @@ export declare const EnvSummarySchema: z.ZodObject<{
190
306
  serverName: z.ZodString;
191
307
  createdAt: z.ZodString;
192
308
  createdByEmail: z.ZodOptional<z.ZodString>;
309
+ ownerSubject: z.ZodOptional<z.ZodString>;
193
310
  deploymentSource: z.ZodOptional<z.ZodString>;
194
311
  accessMode: z.ZodEnum<{
195
312
  "owner-only": "owner-only";
@@ -233,6 +350,7 @@ export declare const EnvsListResponseSchema: z.ZodObject<{
233
350
  serverName: z.ZodString;
234
351
  createdAt: z.ZodString;
235
352
  createdByEmail: z.ZodOptional<z.ZodString>;
353
+ ownerSubject: z.ZodOptional<z.ZodString>;
236
354
  deploymentSource: z.ZodOptional<z.ZodString>;
237
355
  accessMode: z.ZodEnum<{
238
356
  "owner-only": "owner-only";
@@ -277,6 +395,7 @@ export declare const EnvResponseSchema: z.ZodObject<{
277
395
  serverName: z.ZodString;
278
396
  createdAt: z.ZodString;
279
397
  createdByEmail: z.ZodOptional<z.ZodString>;
398
+ ownerSubject: z.ZodOptional<z.ZodString>;
280
399
  deploymentSource: z.ZodOptional<z.ZodString>;
281
400
  accessMode: z.ZodEnum<{
282
401
  "owner-only": "owner-only";
@@ -358,6 +477,7 @@ export declare const DeploymentResponseSchema: z.ZodObject<{
358
477
  serverName: z.ZodString;
359
478
  createdAt: z.ZodString;
360
479
  createdByEmail: z.ZodOptional<z.ZodString>;
480
+ ownerSubject: z.ZodOptional<z.ZodString>;
361
481
  deploymentSource: z.ZodOptional<z.ZodString>;
362
482
  accessMode: z.ZodEnum<{
363
483
  "owner-only": "owner-only";