@noodleseed/one 0.136.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.
- package/dist/commands/assistant-ops.d.ts.map +1 -1
- package/dist/commands/assistant-ops.js +30 -1
- package/dist/commands/assistant-ops.js.map +1 -1
- package/dist/commands/deploy-first-flow.d.ts +2 -0
- package/dist/commands/deploy-first-flow.d.ts.map +1 -1
- package/dist/commands/deploy-first-flow.js +46 -14
- package/dist/commands/deploy-first-flow.js.map +1 -1
- package/dist/commands/deploy-ops.d.ts.map +1 -1
- package/dist/commands/deploy-ops.js +27 -1
- package/dist/commands/deploy-ops.js.map +1 -1
- package/dist/commands/deploy-preflight.d.ts +1 -0
- package/dist/commands/deploy-preflight.d.ts.map +1 -1
- package/dist/commands/deploy-preflight.js +1 -0
- package/dist/commands/deploy-preflight.js.map +1 -1
- package/dist/commands/deploy-status-ops.d.ts +1 -0
- package/dist/commands/deploy-status-ops.d.ts.map +1 -1
- package/dist/commands/deploy-status-ops.js +59 -4
- package/dist/commands/deploy-status-ops.js.map +1 -1
- package/dist/commands/deployments-ops.d.ts.map +1 -1
- package/dist/commands/deployments-ops.js +2 -0
- package/dist/commands/deployments-ops.js.map +1 -1
- package/dist/commands/diagnostics-ops.d.ts +1 -0
- package/dist/commands/diagnostics-ops.d.ts.map +1 -1
- package/dist/commands/diagnostics-ops.js +3 -0
- package/dist/commands/diagnostics-ops.js.map +1 -1
- package/dist/deploy.d.ts +3 -0
- package/dist/deploy.d.ts.map +1 -1
- package/dist/deploy.js +4 -0
- package/dist/deploy.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/node_modules/@noodle-borg/admission-limits/dist/envelope.js +1 -1
- package/node_modules/@noodle-borg/agent-kit/dist/generated/example-files.js +4 -4
- package/node_modules/@noodle-borg/agent-kit/dist/generated/surface.js +1 -0
- package/node_modules/@noodle-borg/agent-kit/dist/skill-embedded-assistant-ref.js +22 -13
- package/node_modules/@noodle-borg/agent-kit/package.json +1 -1
- package/node_modules/@noodle-borg/assistant-gateway/dist/assistant-store.d.ts +6 -2
- package/node_modules/@noodle-borg/assistant-gateway/dist/assistant-store.js +5 -2
- package/node_modules/@noodle-borg/assistant-gateway/dist/model-request.js +37 -0
- package/node_modules/@noodle-borg/assistant-gateway/dist/model-runtime.js +4 -0
- package/node_modules/@noodle-borg/{service/dist/assistant-model-stream.js → assistant-gateway/dist/model-stream.js} +34 -2
- package/node_modules/@noodle-borg/assistant-gateway/dist/public-session.d.ts +3 -0
- package/node_modules/@noodle-borg/assistant-gateway/dist/public-session.js +2 -1
- package/node_modules/@noodle-borg/assistant-gateway/dist/public-turn.d.ts +4 -1
- package/node_modules/@noodle-borg/assistant-gateway/dist/public-turn.js +2 -1
- package/node_modules/@noodle-borg/assistant-gateway/dist/surface-budget.d.ts +6 -1
- package/node_modules/@noodle-borg/assistant-gateway/dist/surface-budget.js +11 -5
- package/node_modules/@noodle-borg/assistant-gateway/package.json +5 -1
- package/node_modules/@noodle-borg/authoring/dist/assistant.d.ts +10 -2
- package/node_modules/@noodle-borg/authoring/dist/assistant.js +4 -0
- package/node_modules/@noodle-borg/authoring/dist/index.d.ts +1 -1
- package/node_modules/@noodle-borg/authoring/dist/index.js +1 -1
- package/node_modules/@noodle-borg/authoring/dist/load-authored.js +1 -1
- package/node_modules/@noodle-borg/cli-catalog/dist/catalog-data-assistant.js +17 -0
- package/node_modules/@noodle-borg/cli-catalog/dist/catalog-data-hosted-deployment.js +14 -0
- package/node_modules/@noodle-borg/compiler/dist/artifact/types.d.ts +1 -7
- package/node_modules/@noodle-borg/compiler/dist/manifest/schema.d.ts +20 -8
- package/node_modules/@noodle-borg/compiler/dist/manifest/schema.js +12 -8
- package/node_modules/@noodle-borg/control-plane/dist/portable.d.ts +2 -0
- package/node_modules/@noodle-borg/control-plane/dist/portable.js +1 -0
- package/node_modules/@noodle-borg/control-plane/dist/rollback-deployment.d.ts +61 -0
- package/node_modules/@noodle-borg/{service/dist/operations → control-plane/dist}/rollback-deployment.js +3 -2
- package/node_modules/@noodle-borg/control-plane/dist/rollback-result.d.ts +19 -0
- package/node_modules/@noodle-borg/control-plane/package.json +3 -1
- package/node_modules/@noodle-borg/developer-mcp/dist/contracts.js +4 -0
- package/node_modules/@noodle-borg/developer-mcp/dist/widgets/app-overview.js +1 -1
- package/node_modules/@noodle-borg/developer-mcp/dist/widgets/deployment-detail.js +1 -1
- package/node_modules/@noodle-borg/module/dist/contract.d.ts +1 -0
- package/node_modules/@noodle-borg/module/dist/deployment-automation.d.ts +1 -1
- package/node_modules/@noodle-borg/module/dist/endpoint-url.d.ts +11 -0
- package/node_modules/@noodle-borg/module/dist/endpoint-url.js +20 -0
- package/node_modules/@noodle-borg/module/dist/index.d.ts +1 -0
- package/node_modules/@noodle-borg/module/dist/index.js +1 -0
- package/node_modules/@noodle-borg/observability/dist/assistant-usage.js +217 -0
- package/node_modules/@noodle-borg/observability/dist/index.js +2 -0
- package/node_modules/@noodle-borg/{service → observability}/dist/request-event-metrics.js +5 -2
- package/node_modules/@noodle-borg/observability/dist/request-events-postgres.js +6 -0
- package/node_modules/@noodle-borg/observability/dist/request-events.js +2 -0
- package/node_modules/@noodle-borg/service/dist/assistant-bindings.js +3 -1
- package/node_modules/@noodle-borg/service/dist/developer-mcp/control-plane-adapter.js +3 -3
- package/node_modules/@noodle-borg/service/dist/developer-mcp/inspection.js +2 -1
- package/node_modules/@noodle-borg/service/dist/registry-access.js +73 -24
- package/node_modules/@noodle-borg/service/dist/registry-deploy-transaction.js +5 -2
- package/node_modules/@noodle-borg/service/dist/registry-helpers.d.ts +5 -9
- package/node_modules/@noodle-borg/service/dist/registry-helpers.js +4 -18
- package/node_modules/@noodle-borg/service/dist/registry-rollback.js +3 -1
- package/node_modules/@noodle-borg/service/dist/registry-status.js +4 -1
- package/node_modules/@noodle-borg/service/dist/registry-targets.js +4 -4
- package/node_modules/@noodle-borg/service/dist/registry-types.d.ts +11 -16
- package/node_modules/@noodle-borg/service/dist/registry.d.ts +8 -5
- package/node_modules/@noodle-borg/service/dist/registry.js +8 -5
- package/node_modules/@noodle-borg/service/dist/routes/access.js +19 -1
- package/node_modules/@noodle-borg/service/dist/routes/analytics-dispatch.js +17 -1
- package/node_modules/@noodle-borg/service/dist/routes/analytics.js +1 -1
- package/node_modules/@noodle-borg/service/dist/routes/assistant-agent.js +73 -59
- package/node_modules/@noodle-borg/service/dist/routes/assistant-doctor.js +1 -1
- package/node_modules/@noodle-borg/service/dist/routes/assistant-model-binding.js +28 -0
- package/node_modules/@noodle-borg/service/dist/routes/assistant-public-session.js +18 -0
- package/node_modules/@noodle-borg/service/dist/routes/assistant-public-turn.js +6 -0
- package/node_modules/@noodle-borg/service/dist/routes/assistant-usage.js +42 -0
- package/node_modules/@noodle-borg/service/dist/routes/assistant.js +20 -3
- package/node_modules/@noodle-borg/service/dist/routes/auth-doctor-live.js +1 -2
- package/node_modules/@noodle-borg/service/dist/routes/control-plane.js +49 -4
- package/node_modules/@noodle-borg/service/dist/routes/deploy-preflight.js +11 -1
- package/node_modules/@noodle-borg/service/dist/routes/endpoint-enrichment.js +2 -1
- package/node_modules/@noodle-borg/service/dist/routes/paths.js +4 -0
- package/node_modules/@noodle-borg/service/dist/routes/rollback.js +1 -1
- package/node_modules/@noodle-borg/service/dist/service.js +6 -0
- package/node_modules/@noodle-borg/service/dist/store/in-memory.d.ts +2 -5
- package/node_modules/@noodle-borg/service/dist/store/in-memory.js +9 -21
- package/node_modules/@noodle-borg/service/dist/store/json-file.d.ts +2 -5
- package/node_modules/@noodle-borg/service/dist/store/json-file.js +8 -20
- package/node_modules/@noodle-borg/service/dist/store/records.js +39 -0
- package/node_modules/@noodle-borg/service/dist/store.d.ts +21 -10
- package/node_modules/@noodle-borg/service/package.json +5 -1
- package/node_modules/@noodle-borg/wire-contracts/dist/control-plane.d.ts +120 -0
- package/node_modules/@noodle-borg/wire-contracts/dist/control-plane.js +123 -1
- package/node_modules/@noodle-borg/wire-contracts/dist/index.d.ts +4 -0
- package/node_modules/@noodle-borg/wire-contracts/dist/index.js +15 -1
- package/node_modules/@noodleseed/assistant/README.md +43 -8
- package/node_modules/@noodleseed/assistant/package.json +16 -2
- package/package.json +2 -2
|
@@ -8,11 +8,12 @@ import { baseFromRequest, readDeployBody, sendForbidden, sendJson, sendUnauthori
|
|
|
8
8
|
import { endpointUrlOptionsForOrg } from '../mcp-public-routing.js';
|
|
9
9
|
import { ensurePersonalWorkspace } from '../personal-workspace.js';
|
|
10
10
|
import { manifestUsesUserRoot } from '../registry-access.js';
|
|
11
|
-
import { tenantMcpUrl } from '../registry-helpers.js';
|
|
12
11
|
import { isIdentityAccessMode } from './access-mode.js';
|
|
13
12
|
import { validateDeployIdempotency } from './deploy-idempotency.js';
|
|
14
13
|
import { provisionPublicEmbed } from './deploy-public-embed.js';
|
|
15
14
|
import { respondDeploymentActivationError } from './deployment-activation-response.js';
|
|
15
|
+
import { tenantMcpUrl } from './endpoint-enrichment.js';
|
|
16
|
+
import { canManageMembers } from './org-admin.js';
|
|
16
17
|
export { handleConfigValues } from './config-values.js';
|
|
17
18
|
export async function handleDeploy(req, res, registry, options, maxBody, tenant, gate, controlPlane, audit) {
|
|
18
19
|
// GHD-3: a GitHub run deploys with a run-scoped token on x-noodle-run-token — NOT a control-plane
|
|
@@ -66,6 +67,7 @@ export async function handleDeploy(req, res, registry, options, maxBody, tenant,
|
|
|
66
67
|
let declaredAccessMode;
|
|
67
68
|
let orgMembershipSources;
|
|
68
69
|
let deploymentSource;
|
|
70
|
+
let requestedOwnerSubject, ownerSubject;
|
|
69
71
|
let serverVersion;
|
|
70
72
|
try {
|
|
71
73
|
const raw = JSON.parse(body.text);
|
|
@@ -88,6 +90,7 @@ export async function handleDeploy(req, res, registry, options, maxBody, tenant,
|
|
|
88
90
|
orgMembershipSources = parsed.data.orgMembershipSources;
|
|
89
91
|
hostedAssets = parsed.data.hostedAssets;
|
|
90
92
|
deploymentSource = parsed.data.deploymentSource;
|
|
93
|
+
requestedOwnerSubject = parsed.data.ownerSubject;
|
|
91
94
|
serverVersion =
|
|
92
95
|
parsed.data.serverVersion === undefined
|
|
93
96
|
? '1'
|
|
@@ -96,6 +99,18 @@ export async function handleDeploy(req, res, registry, options, maxBody, tenant,
|
|
|
96
99
|
catch (error) {
|
|
97
100
|
return sendJson(res, 400, { error: `invalid deploy request: ${error.message}` });
|
|
98
101
|
}
|
|
102
|
+
if (isRunDeploy && requestedOwnerSubject !== undefined) {
|
|
103
|
+
return sendJson(res, 400, {
|
|
104
|
+
error: 'automated deploys cannot select an explicit deployment owner',
|
|
105
|
+
code: 'owner_subject_not_allowed_for_automation',
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
if (!isRunDeploy) {
|
|
109
|
+
const selection = await authorizeDeploymentOwnerSelection(res, controlPlane, tenant.org, accessMode, identity, requestedOwnerSubject);
|
|
110
|
+
if (!selection.ok)
|
|
111
|
+
return;
|
|
112
|
+
ownerSubject = selection.ownerSubject;
|
|
113
|
+
}
|
|
99
114
|
// GHD-3 trusted-phase auth (ADR 0132 Decision 3/4): a run deploy is authorized entirely by its
|
|
100
115
|
// run token plus the access mode resolved here — the control-plane identity was skipped above.
|
|
101
116
|
// Every rejection carries a machine-readable `code` so `noodle github runs <id>` can name the
|
|
@@ -119,9 +134,6 @@ export async function handleDeploy(req, res, registry, options, maxBody, tenant,
|
|
|
119
134
|
...(provenance?.ownerSubject === undefined
|
|
120
135
|
? {}
|
|
121
136
|
: { previousOwnerSubject: provenance.ownerSubject }),
|
|
122
|
-
...(provenance?.ownerEmail === undefined
|
|
123
|
-
? {}
|
|
124
|
-
: { previousOwnerEmail: provenance.ownerEmail }),
|
|
125
137
|
});
|
|
126
138
|
if (automation.kind === 'denied') {
|
|
127
139
|
return sendJson(res, automation.status, {
|
|
@@ -145,6 +157,7 @@ export async function handleDeploy(req, res, registry, options, maxBody, tenant,
|
|
|
145
157
|
accessMode = automation.accessMode;
|
|
146
158
|
orgMembershipSources ??= provenance?.orgMembershipSources;
|
|
147
159
|
identity = automation.actor;
|
|
160
|
+
ownerSubject = automation.ownerSubject;
|
|
148
161
|
automationId = automation.automationId;
|
|
149
162
|
}
|
|
150
163
|
else {
|
|
@@ -154,6 +167,12 @@ export async function handleDeploy(req, res, registry, options, maxBody, tenant,
|
|
|
154
167
|
});
|
|
155
168
|
}
|
|
156
169
|
}
|
|
170
|
+
if (isRunDeploy && accessMode === 'owner-only' && ownerSubject === undefined) {
|
|
171
|
+
return sendJson(res, 400, {
|
|
172
|
+
error: 'owner-only automation requires an existing human owner to preserve',
|
|
173
|
+
code: 'owner_only_unsupported_for_automation',
|
|
174
|
+
});
|
|
175
|
+
}
|
|
157
176
|
// Identity access modes bind the data plane to verified identity, so the deployer must be authenticated.
|
|
158
177
|
// (Localhost dev runs the gate open; such deploys are rejected for lack of an owner/audit actor.)
|
|
159
178
|
if (isIdentityAccessMode(accessMode) && !identity) {
|
|
@@ -238,6 +257,7 @@ export async function handleDeploy(req, res, registry, options, maxBody, tenant,
|
|
|
238
257
|
connectors,
|
|
239
258
|
actor: identity || undefined,
|
|
240
259
|
accessMode,
|
|
260
|
+
ownerSubject,
|
|
241
261
|
...(orgMembershipSources !== undefined ? { orgMembershipSources } : {}),
|
|
242
262
|
hostedAssets,
|
|
243
263
|
serverVersion,
|
|
@@ -339,6 +359,7 @@ export async function handleDeploy(req, res, registry, options, maxBody, tenant,
|
|
|
339
359
|
...(identity?.email !== undefined ? { actorEmail: identity.email } : {}),
|
|
340
360
|
details: {
|
|
341
361
|
accessMode: result.accessMode ?? accessMode,
|
|
362
|
+
...(ownerSubject !== undefined ? { ownerSubject } : {}),
|
|
342
363
|
serverVersion: result.serverVersion,
|
|
343
364
|
hasConnectors: connectors !== undefined,
|
|
344
365
|
},
|
|
@@ -388,6 +409,7 @@ export async function handleDeploy(req, res, registry, options, maxBody, tenant,
|
|
|
388
409
|
deploymentId: result.deploymentId,
|
|
389
410
|
serverVersion: result.serverVersion,
|
|
390
411
|
accessMode: result.accessMode,
|
|
412
|
+
...(result.ownerSubject !== undefined ? { ownerSubject: result.ownerSubject } : {}),
|
|
391
413
|
url: tenantMcpUrl(base, tenant, result.serverVersion, endpointOptions),
|
|
392
414
|
defaultUrl: tenantMcpUrl(base, tenant, undefined, endpointOptions),
|
|
393
415
|
...(embedId !== undefined ? { embedId } : {}),
|
|
@@ -398,6 +420,29 @@ function normalizeParsedServerVersion(value) {
|
|
|
398
420
|
throw new Error('"serverVersion" must be a version string');
|
|
399
421
|
return normalizeServerVersion(value);
|
|
400
422
|
}
|
|
423
|
+
export async function authorizeDeploymentOwnerSelection(res, controlPlane, org, accessMode, identity, requestedOwnerSubject) {
|
|
424
|
+
if (accessMode !== 'owner-only')
|
|
425
|
+
return { ok: true };
|
|
426
|
+
if (identity === undefined) {
|
|
427
|
+
sendJson(res, 401, {
|
|
428
|
+
error: 'owner-only deployments require an authenticated deployer',
|
|
429
|
+
code: 'deployer_identity_required',
|
|
430
|
+
});
|
|
431
|
+
return { ok: false };
|
|
432
|
+
}
|
|
433
|
+
if (requestedOwnerSubject === undefined || requestedOwnerSubject === identity.subject) {
|
|
434
|
+
return { ok: true, ownerSubject: identity.subject };
|
|
435
|
+
}
|
|
436
|
+
if (identity.developerGrantId !== undefined ||
|
|
437
|
+
!(await canManageMembers(controlPlane, org, identity))) {
|
|
438
|
+
sendJson(res, 403, {
|
|
439
|
+
error: 'Selecting a different deployment owner requires organization owner access.',
|
|
440
|
+
code: 'deployment_owner_authorization_required',
|
|
441
|
+
});
|
|
442
|
+
return { ok: false };
|
|
443
|
+
}
|
|
444
|
+
return { ok: true, ownerSubject: requestedOwnerSubject };
|
|
445
|
+
}
|
|
401
446
|
export async function handleWhoami(req, res, gate, controlPlane, options = {}) {
|
|
402
447
|
// Identity introspection names no org or environment, so no grant context can describe it.
|
|
403
448
|
const identity = await authorizeControlPlane(req, res, gate, {
|
|
@@ -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,
|
|
@@ -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,
|
|
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,
|
|
122
|
+
async updateActiveAccess(ref, deploymentId, input) {
|
|
121
123
|
const safe = validateTenantRef(ref);
|
|
122
124
|
const record = this.#records.get(deploymentId);
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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(
|
|
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,
|
|
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,
|
|
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
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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
|
|
267
|
-
*
|
|
268
|
-
*
|
|
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
|
|
298
|
-
*
|
|
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,
|
|
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.
|
|
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",
|