@noodleseed/one 0.167.0 → 0.169.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/deployment-delete-ops.d.ts +12 -0
- package/dist/commands/deployment-delete-ops.d.ts.map +1 -0
- package/dist/commands/deployment-delete-ops.js +230 -0
- package/dist/commands/deployment-delete-ops.js.map +1 -0
- package/dist/commands/deployment-delete-recovery.d.ts +35 -0
- package/dist/commands/deployment-delete-recovery.d.ts.map +1 -0
- package/dist/commands/deployment-delete-recovery.js +99 -0
- package/dist/commands/deployment-delete-recovery.js.map +1 -0
- package/dist/commands/deployments-ops.d.ts +2 -1
- package/dist/commands/deployments-ops.d.ts.map +1 -1
- package/dist/commands/deployments-ops.js +5 -1
- package/dist/commands/deployments-ops.js.map +1 -1
- package/node_modules/@noodle-borg/agent-kit/dist/behavior-skills.js +4 -2
- package/node_modules/@noodle-borg/agent-kit/dist/plugin-bootstrap-skill.js +8 -1
- package/node_modules/@noodle-borg/agent-kit/dist/skill-embedded-assistant-ref.js +20 -0
- package/node_modules/@noodle-borg/agent-kit/dist/skill-router.js +9 -2
- package/node_modules/@noodle-borg/agent-kit/package.json +1 -1
- package/node_modules/@noodle-borg/cli-catalog/dist/catalog-data-tenant-resources.js +27 -1
- package/node_modules/@noodle-borg/control-plane/dist/delete-deployment.d.ts +44 -0
- package/node_modules/@noodle-borg/control-plane/dist/delete-deployment.js +105 -0
- package/node_modules/@noodle-borg/control-plane/dist/deployment-deletion-contracts.d.ts +20 -0
- 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/service/dist/deployment-deletion.js +40 -0
- package/node_modules/@noodle-borg/service/dist/registry-compile.js +6 -0
- package/node_modules/@noodle-borg/service/dist/registry-deletion.js +75 -0
- package/node_modules/@noodle-borg/service/dist/registry-state.js +4 -2
- package/node_modules/@noodle-borg/service/dist/registry-targets.js +4 -0
- package/node_modules/@noodle-borg/service/dist/registry.d.ts +2 -0
- package/node_modules/@noodle-borg/service/dist/registry.js +7 -23
- package/node_modules/@noodle-borg/service/dist/routes/deploy-dispatch.js +15 -1
- package/node_modules/@noodle-borg/service/dist/routes/deployment-delete.js +53 -0
- package/node_modules/@noodle-borg/service/dist/store/in-memory.d.ts +2 -1
- package/node_modules/@noodle-borg/service/dist/store/in-memory.js +8 -0
- package/node_modules/@noodle-borg/service/dist/store/json-file-deletion.js +64 -0
- package/node_modules/@noodle-borg/service/dist/store/json-file.d.ts +2 -1
- package/node_modules/@noodle-borg/service/dist/store/json-file.js +23 -5
- package/node_modules/@noodle-borg/service/dist/store.d.ts +5 -1
- package/node_modules/@noodle-borg/service/package.json +1 -1
- package/node_modules/@noodle-borg/wire-contracts/dist/deployment-deletion.d.ts +29 -0
- package/node_modules/@noodle-borg/wire-contracts/dist/deployment-deletion.js +33 -0
- package/node_modules/@noodle-borg/wire-contracts/dist/index.d.ts +1 -0
- package/node_modules/@noodle-borg/wire-contracts/dist/index.js +1 -0
- package/package.json +2 -2
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
// one-level reference tree rendered by skill-content.ts.
|
|
4
4
|
import { BEHAVIOR_SKILLS } from './behavior-skills.js';
|
|
5
5
|
export const SKILL_DESCRIPTION = 'Use when building, validating, testing, deploying, or operating a local or hosted Noodle Seed ' +
|
|
6
|
-
'MCP server or app authored in TypeScript with the noodle CLI
|
|
6
|
+
'MCP server or app authored in TypeScript with the noodle CLI, or improving signup or onboarding ' +
|
|
7
|
+
'conversion or selecting a first Noodle workflow for an account-growth product.';
|
|
7
8
|
export const APP_DIRECTORY_COMPLIANCE_REFERENCE = 'references/app-directory-compliance.md';
|
|
8
9
|
export const SKILL_ROUTES = [
|
|
9
10
|
{
|
|
@@ -68,7 +69,7 @@ export const SKILL_ROUTES = [
|
|
|
68
69
|
},
|
|
69
70
|
{
|
|
70
71
|
id: 'embed-assistant',
|
|
71
|
-
intent: 'Embed a Noodle assistant in an existing SaaS or web application',
|
|
72
|
+
intent: 'Embed a Noodle assistant in an existing SaaS or web application, including public-to-product continuous onboarding',
|
|
72
73
|
skill: 'embedding-mcp-assistants',
|
|
73
74
|
exitCondition: 'The requested embed boundary works with verified identity and credential separation at the tested level.',
|
|
74
75
|
},
|
|
@@ -147,6 +148,12 @@ export function skillRouterBody(_target) {
|
|
|
147
148
|
'',
|
|
148
149
|
'Negative routing examples: “Inspect hosted logs/status” → `inspect-hosted` (read-only). “Prepare for deployment” → the applicable build or verification route; with existing access it may inspect `deploy preflight`, then stop with a handoff. Preparation does not authorize `link`, hosted config, publication, rollback, host writes, or submission. “Keep this local” → a build or verification route, never a hosted route.',
|
|
149
150
|
'',
|
|
151
|
+
'## First-workflow heuristic',
|
|
152
|
+
'',
|
|
153
|
+
'When the user asks what to build first or gives only a broad growth goal, inspect the repository and product context before asking them to invent a workflow. Do not classify the business or choose a design from an industry label alone.',
|
|
154
|
+
'',
|
|
155
|
+
'If the evidence shows a public visitor surface, an existing signup or sign-in boundary, a useful pre-account result the assistant can genuinely produce, and a meaningful authenticated outcome inside the product—and more authenticated users are valuable—recommend public-to-product continuous onboarding and route to `embedding-mcp-assistants`. This recommendation is not limited to B2B SaaS. If one of those facts is unclear, ask only for the smallest missing fit fact; if the pattern does not fit, route by the user’s actual outcome instead.',
|
|
156
|
+
'',
|
|
150
157
|
...routeTableLines(),
|
|
151
158
|
'',
|
|
152
159
|
'## Common machine loop',
|
|
@@ -174,7 +174,7 @@ export const CATALOG_TENANT_RESOURCES = [
|
|
|
174
174
|
{
|
|
175
175
|
name: 'deployments',
|
|
176
176
|
section: 'resources',
|
|
177
|
-
summary: 'List, inspect,
|
|
177
|
+
summary: 'List, inspect, read packages, or permanently delete deployment records.',
|
|
178
178
|
arguments: [],
|
|
179
179
|
flags: [],
|
|
180
180
|
subcommands: [
|
|
@@ -203,6 +203,32 @@ export const CATALOG_TENANT_RESOURCES = [
|
|
|
203
203
|
flags: DEPLOYMENT_INSPECT_FLAGS,
|
|
204
204
|
jsonOutput: { mode: 'single' },
|
|
205
205
|
},
|
|
206
|
+
{
|
|
207
|
+
name: 'delete',
|
|
208
|
+
summary: 'Permanently delete one inactive deployment record.',
|
|
209
|
+
arguments: [
|
|
210
|
+
{
|
|
211
|
+
...REQUIRED_ARGUMENT,
|
|
212
|
+
name: 'deployment-id',
|
|
213
|
+
summary: 'Inactive deployment identifier to delete permanently.',
|
|
214
|
+
},
|
|
215
|
+
],
|
|
216
|
+
flags: [ORG, SERVICE, AUTH_TOKEN, YES, JSON_FLAG],
|
|
217
|
+
jsonOutput: { mode: 'single' },
|
|
218
|
+
},
|
|
219
|
+
{
|
|
220
|
+
name: 'delete-version',
|
|
221
|
+
summary: 'Permanently delete every deployment record for one exact version.',
|
|
222
|
+
arguments: [
|
|
223
|
+
{
|
|
224
|
+
...REQUIRED_ARGUMENT,
|
|
225
|
+
name: 'version-or-legacy',
|
|
226
|
+
summary: 'Exact numeric server version, or legacy for unversioned deployments.',
|
|
227
|
+
},
|
|
228
|
+
],
|
|
229
|
+
flags: [...DEPLOYMENT_BASE_FLAGS, YES, JSON_FLAG],
|
|
230
|
+
jsonOutput: { mode: 'single' },
|
|
231
|
+
},
|
|
206
232
|
{
|
|
207
233
|
name: 'lock',
|
|
208
234
|
summary: 'Freeze an active server version against deploys and rollbacks.',
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { AuditSink, DeploymentOperationTarget } from '@noodle-borg/module';
|
|
2
|
+
import type { DeploymentDeleteResponse } from '@noodle-borg/wire-contracts';
|
|
3
|
+
import type { ControlPlaneIdentity } from './contracts.js';
|
|
4
|
+
import type { DeploymentDeleteResult, DeploymentDeleteSelection } from './deployment-deletion-contracts.js';
|
|
5
|
+
import type { RollbackControlPlane } from './rollback-deployment.js';
|
|
6
|
+
export type DeploymentDeleteScope = {
|
|
7
|
+
readonly kind: 'deployment';
|
|
8
|
+
readonly org: string;
|
|
9
|
+
readonly deploymentId: string;
|
|
10
|
+
} | {
|
|
11
|
+
readonly kind: 'version';
|
|
12
|
+
readonly org: string;
|
|
13
|
+
readonly target: DeploymentOperationTarget;
|
|
14
|
+
readonly serverVersion?: string;
|
|
15
|
+
};
|
|
16
|
+
export interface DeploymentDeletionRegistry {
|
|
17
|
+
getDeployment(org: string, deploymentId: string): Promise<{
|
|
18
|
+
readonly orgSlug: string;
|
|
19
|
+
readonly appSlug: string;
|
|
20
|
+
readonly environment: string;
|
|
21
|
+
} | undefined>;
|
|
22
|
+
deleteDeployments(target: DeploymentOperationTarget, selection: DeploymentDeleteSelection): Promise<DeploymentDeleteResult>;
|
|
23
|
+
}
|
|
24
|
+
export interface DeploymentDeletionDependencies {
|
|
25
|
+
readonly registry: DeploymentDeletionRegistry;
|
|
26
|
+
readonly controlPlane: RollbackControlPlane;
|
|
27
|
+
readonly audit: AuditSink;
|
|
28
|
+
}
|
|
29
|
+
export type DeploymentDeleteOperationResult = {
|
|
30
|
+
readonly ok: true;
|
|
31
|
+
readonly view: DeploymentDeleteResponse;
|
|
32
|
+
} | {
|
|
33
|
+
readonly ok: false;
|
|
34
|
+
readonly status: 400 | 403 | 404 | 409;
|
|
35
|
+
readonly code: string;
|
|
36
|
+
readonly error: string;
|
|
37
|
+
};
|
|
38
|
+
/** One owner-authorized deletion path for every host; stores commit the exact selection atomically. */
|
|
39
|
+
export declare function deleteDeploymentOperation(deps: DeploymentDeletionDependencies, input: {
|
|
40
|
+
readonly actor: ControlPlaneIdentity;
|
|
41
|
+
readonly scope: DeploymentDeleteScope;
|
|
42
|
+
readonly expectedDeploymentIds?: readonly string[];
|
|
43
|
+
}): Promise<DeploymentDeleteOperationResult>;
|
|
44
|
+
//# sourceMappingURL=delete-deployment.d.ts.map
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
const ERRORS = {
|
|
2
|
+
deployment_not_found: 'Deployment not found.',
|
|
3
|
+
version_not_found: 'Version not found.',
|
|
4
|
+
active_deployment: 'Roll back to another deployment before deleting the live deployment, or delete the whole version.',
|
|
5
|
+
deployment_locked: 'Unlock this version before deleting it.',
|
|
6
|
+
app_archived: 'Restore the archived app before deleting its deployments.',
|
|
7
|
+
deployment_delete_conflict: 'The deployment history changed. Refresh and review it before deleting.',
|
|
8
|
+
};
|
|
9
|
+
/** One owner-authorized deletion path for every host; stores commit the exact selection atomically. */
|
|
10
|
+
export async function deleteDeploymentOperation(deps, input) {
|
|
11
|
+
const { actor: identity, scope: route } = input;
|
|
12
|
+
const org = route.kind === 'version' ? route.target.org : route.org;
|
|
13
|
+
const member = identity.superAdmin
|
|
14
|
+
? undefined
|
|
15
|
+
: await deps.controlPlane.getOrgMember({ org, subject: identity.subject });
|
|
16
|
+
if (identity.developerGrantId !== undefined ||
|
|
17
|
+
(!identity.superAdmin && member?.role !== 'owner')) {
|
|
18
|
+
return {
|
|
19
|
+
ok: false,
|
|
20
|
+
status: 403,
|
|
21
|
+
code: 'organization_owner_required',
|
|
22
|
+
error: 'Only an organization owner can delete deployments or versions.',
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
let target;
|
|
26
|
+
let selection;
|
|
27
|
+
if (route.kind === 'deployment') {
|
|
28
|
+
const record = await deps.registry.getDeployment(route.org, route.deploymentId);
|
|
29
|
+
if (!record)
|
|
30
|
+
return {
|
|
31
|
+
ok: false,
|
|
32
|
+
status: 404,
|
|
33
|
+
code: 'deployment_not_found',
|
|
34
|
+
error: ERRORS.deployment_not_found,
|
|
35
|
+
};
|
|
36
|
+
target = { org: record.orgSlug, app: record.appSlug, env: record.environment };
|
|
37
|
+
selection = { kind: 'deployment', deploymentId: route.deploymentId };
|
|
38
|
+
}
|
|
39
|
+
else {
|
|
40
|
+
if (!input.expectedDeploymentIds?.length)
|
|
41
|
+
return {
|
|
42
|
+
ok: false,
|
|
43
|
+
status: 400,
|
|
44
|
+
code: 'invalid_deletion_confirmation',
|
|
45
|
+
error: 'Invalid version deletion confirmation.',
|
|
46
|
+
};
|
|
47
|
+
target = route.target;
|
|
48
|
+
selection = {
|
|
49
|
+
kind: 'version',
|
|
50
|
+
expectedDeploymentIds: input.expectedDeploymentIds,
|
|
51
|
+
...(route.serverVersion === undefined ? {} : { serverVersion: route.serverVersion }),
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
const result = await deps.registry.deleteDeployments(target, selection);
|
|
55
|
+
const status = result.ok
|
|
56
|
+
? 200
|
|
57
|
+
: result.code === 'deployment_not_found' || result.code === 'version_not_found'
|
|
58
|
+
? 404
|
|
59
|
+
: 409;
|
|
60
|
+
let auditRecorded = true;
|
|
61
|
+
try {
|
|
62
|
+
await deps.audit.emit({
|
|
63
|
+
eventType: result.ok
|
|
64
|
+
? route.kind === 'version'
|
|
65
|
+
? 'deployment.version.deleted'
|
|
66
|
+
: 'deployment.deleted'
|
|
67
|
+
: 'deployment.delete.rejected',
|
|
68
|
+
org: target.org,
|
|
69
|
+
app: target.app,
|
|
70
|
+
env: target.env,
|
|
71
|
+
...(route.kind === 'deployment' ? { deploymentId: route.deploymentId } : {}),
|
|
72
|
+
actorSubject: identity.subject,
|
|
73
|
+
...(identity.email === undefined ? {} : { actorEmail: identity.email }),
|
|
74
|
+
decision: result.ok ? 'allow' : 'deny',
|
|
75
|
+
status,
|
|
76
|
+
...(!result.ok ? { reasonCode: result.code } : {}),
|
|
77
|
+
details: {
|
|
78
|
+
scope: route.kind,
|
|
79
|
+
...(route.kind === 'version' ? { serverVersion: route.serverVersion ?? 'legacy' } : {}),
|
|
80
|
+
...(result.ok ? { deletedDeployments: result.deleted.length } : {}),
|
|
81
|
+
},
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
// Deletion has already committed. An audit outage must not claim the records still exist.
|
|
86
|
+
auditRecorded = false;
|
|
87
|
+
}
|
|
88
|
+
if (!result.ok)
|
|
89
|
+
return {
|
|
90
|
+
ok: false,
|
|
91
|
+
status: status,
|
|
92
|
+
code: result.code,
|
|
93
|
+
error: ERRORS[result.code],
|
|
94
|
+
};
|
|
95
|
+
return {
|
|
96
|
+
ok: true,
|
|
97
|
+
view: {
|
|
98
|
+
ok: true,
|
|
99
|
+
target,
|
|
100
|
+
deletedDeploymentIds: result.deleted.map((record) => record.deploymentId),
|
|
101
|
+
auditRecorded,
|
|
102
|
+
},
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
//# sourceMappingURL=delete-deployment.js.map
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export type DeploymentDeleteSelection = {
|
|
2
|
+
readonly kind: 'deployment';
|
|
3
|
+
readonly deploymentId: string;
|
|
4
|
+
} | {
|
|
5
|
+
readonly kind: 'version';
|
|
6
|
+
readonly serverVersion?: string;
|
|
7
|
+
readonly expectedDeploymentIds: readonly string[];
|
|
8
|
+
};
|
|
9
|
+
export type DeploymentDeleteResult<Record extends {
|
|
10
|
+
readonly deploymentId: string;
|
|
11
|
+
} = {
|
|
12
|
+
readonly deploymentId: string;
|
|
13
|
+
}> = {
|
|
14
|
+
readonly ok: true;
|
|
15
|
+
readonly deleted: readonly Record[];
|
|
16
|
+
} | {
|
|
17
|
+
readonly ok: false;
|
|
18
|
+
readonly code: 'deployment_not_found' | 'version_not_found' | 'active_deployment' | 'deployment_locked' | 'app_archived' | 'deployment_delete_conflict';
|
|
19
|
+
};
|
|
20
|
+
//# sourceMappingURL=deployment-deletion-contracts.d.ts.map
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
export { type AppPurgeReconciliationActor, type AppPurgeReconciliationConflictCode, AppPurgeReconciliationError, type AppPurgeReconciliationOperator, } from './app-purge-reconciliation-port.js';
|
|
2
2
|
export type { ActiveMcpSubdomainClaim, ChangeMcpSubdomainInput, ControlPlaneIdentity, CreateOrgWithOwnerInput, McpSubdomainMutationResult, McpSubdomainSetting, OrganizationStore, OrgDomainRecord, OrgInvitationRecord, OrgMemberRecord, OrgOpenAIAppsChallengeRecord, OrgRecord, OrgRole, PersonalWorkspaceProvisionInput, PersonalWorkspaceProvisionResult, SignupAllowlistKind, SignupAllowlistRecord, WelcomeEmailRecord, } from './contracts.js';
|
|
3
3
|
export { PersonalWorkspaceOwnerMutationError } from './contracts.js';
|
|
4
|
+
export { type DeploymentDeleteOperationResult, type DeploymentDeleteScope, type DeploymentDeletionDependencies, type DeploymentDeletionRegistry, deleteDeploymentOperation, } from './delete-deployment.js';
|
|
4
5
|
export { allowAllGate, bearerToken, type ControlPlaneAuthResult, type DeployAuthGate, } from './deploy-auth.js';
|
|
5
6
|
export { CompositeControlPlaneGate, type ControlPlaneSignupMode, GoogleControlPlaneGate, type GoogleControlPlaneGateOptions, type GoogleIdTokenVerifier, GoogleWorkloadControlPlaneGate, type GoogleWorkloadControlPlaneGateOptions, NoodleOAuthControlPlaneGate, type NoodleOAuthControlPlaneGateOptions, type SignupAuthorizer, } from './deploy-gates.js';
|
|
7
|
+
export type { DeploymentDeleteResult, DeploymentDeleteSelection, } from './deployment-deletion-contracts.js';
|
|
6
8
|
export { InMemoryControlPlaneStore } from './in-memory-control-plane-store.js';
|
|
7
9
|
export { bindInMemoryOrganizationStore, type InMemoryOrganizationOperations, InMemoryOrganizationStore, } from './in-memory-organization-store.js';
|
|
8
10
|
export { InMemoryMcpSubdomainClaimStore, McpSubdomainCooldownError, McpSubdomainIdempotencyConflictError, McpSubdomainOwnerRequiredError, type McpSubdomainRouteRef, McpSubdomainUnavailableError, mcpSubdomainEndpointOptions, type ResolvedMcpTenantRef, resolveMcpSubdomainTenant, } from './mcp-subdomain-claims.js';
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export { AppPurgeReconciliationError, } from './app-purge-reconciliation-port.js';
|
|
2
2
|
export { PersonalWorkspaceOwnerMutationError } from './contracts.js';
|
|
3
|
+
export { deleteDeploymentOperation, } from './delete-deployment.js';
|
|
3
4
|
export { allowAllGate, bearerToken, } from './deploy-auth.js';
|
|
4
5
|
export { CompositeControlPlaneGate, GoogleControlPlaneGate, GoogleWorkloadControlPlaneGate, NoodleOAuthControlPlaneGate, } from './deploy-gates.js';
|
|
5
6
|
export { InMemoryControlPlaneStore } from './in-memory-control-plane-store.js';
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { normalizeServerVersion } from '@noodle-borg/module';
|
|
2
|
+
import { sameTenantRecord } from './deployment-versioning.js';
|
|
3
|
+
import { validateTenantRef } from './store/validate.js';
|
|
4
|
+
/** Shared, side-effect-free decision made inside each adapter's mutation boundary. */
|
|
5
|
+
export function planDeploymentDeletion(records, ref, selection) {
|
|
6
|
+
const safe = validateTenantRef(ref);
|
|
7
|
+
const version = selection.kind === 'version' && selection.serverVersion !== undefined
|
|
8
|
+
? normalizeServerVersion(selection.serverVersion)
|
|
9
|
+
: undefined;
|
|
10
|
+
const deleted = records.filter((record) => sameTenantRecord(record, safe) &&
|
|
11
|
+
(selection.kind === 'deployment'
|
|
12
|
+
? record.deploymentId === selection.deploymentId
|
|
13
|
+
: record.serverVersion === version));
|
|
14
|
+
if (deleted.length === 0)
|
|
15
|
+
return {
|
|
16
|
+
ok: false,
|
|
17
|
+
code: selection.kind === 'deployment' ? 'deployment_not_found' : 'version_not_found',
|
|
18
|
+
};
|
|
19
|
+
if (records.some((record) => record.orgSlug === safe.org &&
|
|
20
|
+
record.appSlug === safe.app &&
|
|
21
|
+
record.archivedAt !== undefined)) {
|
|
22
|
+
return { ok: false, code: 'app_archived' };
|
|
23
|
+
}
|
|
24
|
+
if (selection.kind === 'deployment') {
|
|
25
|
+
if (deleted.some((record) => record.active))
|
|
26
|
+
return { ok: false, code: 'active_deployment' };
|
|
27
|
+
}
|
|
28
|
+
else {
|
|
29
|
+
if (deleted.some((record) => record.active && record.deploymentLock !== undefined))
|
|
30
|
+
return { ok: false, code: 'deployment_locked' };
|
|
31
|
+
const expected = new Set(selection.expectedDeploymentIds);
|
|
32
|
+
if (expected.size !== selection.expectedDeploymentIds.length ||
|
|
33
|
+
expected.size !== deleted.length ||
|
|
34
|
+
deleted.some((record) => !expected.has(record.deploymentId))) {
|
|
35
|
+
return { ok: false, code: 'deployment_delete_conflict' };
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return { ok: true, deleted };
|
|
39
|
+
}
|
|
40
|
+
//# sourceMappingURL=deployment-deletion.js.map
|
|
@@ -9,6 +9,7 @@ import { ManagedConfigBroker } from './credential-broker.js';
|
|
|
9
9
|
import { deploymentCredentialBrokerOptions } from './credential-broker-options.js';
|
|
10
10
|
import { deploymentRecordVersionError } from './deployment-record-version.js';
|
|
11
11
|
import { normalizePersistedManifestForCompile } from './manifest-normalize.js';
|
|
12
|
+
import { registryRecordStillExists } from './registry-deletion.js';
|
|
12
13
|
import { missingCapabilityErrors, missingSecretErrors, missingVariableErrors, } from './registry-helpers.js';
|
|
13
14
|
import { servedTargetFor } from './registry-targets.js';
|
|
14
15
|
import { createDeploymentStateConnector, } from './state-connector-factory.js';
|
|
@@ -252,6 +253,11 @@ export async function loadPersistedRegistryTarget(source, context) {
|
|
|
252
253
|
if (record.active &&
|
|
253
254
|
(await context.hasCustomerAuthConflict(record, built.served.artifact.server.auth)))
|
|
254
255
|
return undefined;
|
|
256
|
+
const exists = state.store === undefined
|
|
257
|
+
? state.records.has(deploymentId)
|
|
258
|
+
: await registryRecordStillExists(state, deploymentId);
|
|
259
|
+
if (!exists)
|
|
260
|
+
return undefined;
|
|
255
261
|
const target = servedTargetFor(record, built.served, context.customerVerifierFactory);
|
|
256
262
|
state.servers.set(deploymentId, target);
|
|
257
263
|
state.records.set(deploymentId, record);
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { planDeploymentDeletion } from './deployment-deletion.js';
|
|
2
|
+
import { targetForPersistedRecord } from './registry-targets.js';
|
|
3
|
+
import { validateTenantRef } from './store/validate.js';
|
|
4
|
+
export function evictDeletedDeployments(state, ids) {
|
|
5
|
+
for (const id of ids) {
|
|
6
|
+
state.records.delete(id);
|
|
7
|
+
state.servers.delete(id);
|
|
8
|
+
}
|
|
9
|
+
for (const [key, id] of state.activeTenants)
|
|
10
|
+
if (ids.has(id))
|
|
11
|
+
state.activeTenants.delete(key);
|
|
12
|
+
}
|
|
13
|
+
export async function deleteRegistryDeployments(state, ref, selection) {
|
|
14
|
+
const safe = validateTenantRef(ref);
|
|
15
|
+
const result = state.store === undefined
|
|
16
|
+
? planDeploymentDeletion([...state.records.values()], safe, selection)
|
|
17
|
+
: await state.store.deleteDeployments(safe, selection);
|
|
18
|
+
if (result.ok)
|
|
19
|
+
evictDeletedDeployments(state, new Set(result.deleted.map((record) => record.deploymentId)));
|
|
20
|
+
return result;
|
|
21
|
+
}
|
|
22
|
+
/** Async compilation must rejoin authority before publishing a result into the disposable cache. */
|
|
23
|
+
export async function registryRecordStillExists(state, deploymentId) {
|
|
24
|
+
const current = state.store === undefined
|
|
25
|
+
? state.records.get(deploymentId)
|
|
26
|
+
: await state.store.get(deploymentId);
|
|
27
|
+
if (current !== undefined && current.archivedAt === undefined)
|
|
28
|
+
return true;
|
|
29
|
+
evictDeletedDeployments(state, new Set([deploymentId]));
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
/** Refresh warmed cache entries from authority and coalesce compilation on a cache miss. */
|
|
33
|
+
export async function readRegistryTarget(state, deploymentId, inflight, targetForRecord, load) {
|
|
34
|
+
if (state.records.get(deploymentId)?.archivedAt !== undefined)
|
|
35
|
+
return undefined;
|
|
36
|
+
if (state.servers.has(deploymentId)) {
|
|
37
|
+
const record = state.store
|
|
38
|
+
? await state.store.get(deploymentId)
|
|
39
|
+
: state.records.get(deploymentId);
|
|
40
|
+
if (record === undefined)
|
|
41
|
+
evictDeletedDeployments(state, new Set([deploymentId]));
|
|
42
|
+
return record === undefined || record.archivedAt !== undefined
|
|
43
|
+
? undefined
|
|
44
|
+
: targetForRecord(record);
|
|
45
|
+
}
|
|
46
|
+
const pending = inflight.get(deploymentId);
|
|
47
|
+
if (pending)
|
|
48
|
+
return pending;
|
|
49
|
+
const promise = load(deploymentId).finally(() => {
|
|
50
|
+
inflight.delete(deploymentId);
|
|
51
|
+
});
|
|
52
|
+
inflight.set(deploymentId, promise);
|
|
53
|
+
return promise;
|
|
54
|
+
}
|
|
55
|
+
/** No-store records are authority, so stale reconciliation must never repopulate absent records. */
|
|
56
|
+
export async function reconcileRegistryTarget(state, record, context) {
|
|
57
|
+
let loaded = false;
|
|
58
|
+
const target = await targetForPersistedRecord(record, {
|
|
59
|
+
...context,
|
|
60
|
+
load: () => {
|
|
61
|
+
loaded = true;
|
|
62
|
+
return context.load();
|
|
63
|
+
},
|
|
64
|
+
records: state.records,
|
|
65
|
+
servers: state.servers,
|
|
66
|
+
recordStillPresent: () => state.store !== undefined || state.records.has(record.deploymentId),
|
|
67
|
+
});
|
|
68
|
+
// A cold load already checks store authority just before publication; warm reconciliation must too.
|
|
69
|
+
// No async presence snapshot in no-store mode: deletion and publication share one synchronous turn.
|
|
70
|
+
const exists = state.store === undefined
|
|
71
|
+
? state.records.has(record.deploymentId)
|
|
72
|
+
: loaded || (await registryRecordStillExists(state, record.deploymentId));
|
|
73
|
+
return exists ? target : undefined;
|
|
74
|
+
}
|
|
75
|
+
//# sourceMappingURL=registry-deletion.js.map
|
|
@@ -166,8 +166,10 @@ export async function setRegistryDeploymentLock(state, ref, serverVersion, expec
|
|
|
166
166
|
state.records.set(result.record.deploymentId, result.record);
|
|
167
167
|
return result;
|
|
168
168
|
}
|
|
169
|
-
const
|
|
170
|
-
|
|
169
|
+
const observed = await activeRecordVersion(state, safe, safeVersion);
|
|
170
|
+
// The lookup yields; a concurrent deletion or pointer move must win before any cache write.
|
|
171
|
+
const active = observed === undefined ? undefined : state.records.get(observed.deploymentId);
|
|
172
|
+
if (active === undefined || !active.active || active.archivedAt !== undefined)
|
|
171
173
|
return { ok: false, reason: 'no_active_deployment' };
|
|
172
174
|
if (active.deploymentId !== expectedDeploymentId)
|
|
173
175
|
return { ok: false, reason: 'conflict' };
|
|
@@ -110,6 +110,8 @@ function bindAppPackageSnapshot(served, value) {
|
|
|
110
110
|
}
|
|
111
111
|
/** Reconcile one persisted record with its cached served target without trusting stale auth projections. */
|
|
112
112
|
export async function targetForPersistedRecord(record, state) {
|
|
113
|
+
if (state.recordStillPresent?.() === false)
|
|
114
|
+
return undefined;
|
|
113
115
|
const cached = state.servers.get(record.deploymentId);
|
|
114
116
|
const cachedSchemaVersion = cached === undefined ? undefined : targetPolicyVersions.get(cached);
|
|
115
117
|
state.records.set(record.deploymentId, record);
|
|
@@ -127,6 +129,8 @@ export async function targetForPersistedRecord(record, state) {
|
|
|
127
129
|
(await state.hasCustomerAuthConflict(record, cached.served.artifact.server.auth))) {
|
|
128
130
|
return undefined;
|
|
129
131
|
}
|
|
132
|
+
if (state.recordStillPresent?.() === false)
|
|
133
|
+
return undefined;
|
|
130
134
|
if (cachedSchemaVersion === record.schemaVersion && servedTargetMatchesRecord(cached, record))
|
|
131
135
|
return cached;
|
|
132
136
|
const reconciled = servedTargetFor(record, cached.served, state.customerVerifierFactory);
|
|
@@ -9,6 +9,7 @@ import type { ServedTarget } from '@noodle-borg/transport-http';
|
|
|
9
9
|
import type { NativeRecordConnectorFactory } from './native-record-connector.js';
|
|
10
10
|
import { type ActiveDeployProvenance } from './registry-helpers.js';
|
|
11
11
|
import type { AccessUpdateOptions, AccessUpdateResult, DeployOptions, DeployPreflightResult, RecoverResult, RunDeployResult, ServerRegistryOptions } from './registry-types.js';
|
|
12
|
+
import type { DeploymentDeleteResult, DeploymentDeleteSelection } from './store.js';
|
|
12
13
|
import { type AppArchiveResult, type AppRestoreResult, type AppSummary, type ArtifactStore, type ConfigStore, type CustomerAuthRestoreProjection, type DeploymentListFilter, type DeploymentLock, type DeploymentLockUpdateResult, type DeploymentStatus, type DeploymentSummary, type DeployRecord, type EnvSummary, type ProductionEnvironmentChange, type TenantRef } from './store.js';
|
|
13
14
|
export type { RollbackResult } from '@noodle-borg/control-plane/portable';
|
|
14
15
|
export type { DeploymentPackage } from './registry-package.js';
|
|
@@ -29,6 +30,7 @@ export declare class ServerRegistry {
|
|
|
29
30
|
/** Read-only deploy validation used by the CLI before config writes, asset upload, or persistence. */
|
|
30
31
|
preflightDeploy(tenant: TenantRef, manifest: string, options?: DeployOptions): Promise<DeployPreflightResult>;
|
|
31
32
|
recover(): Promise<RecoverResult>;
|
|
33
|
+
deleteDeployments(ref: TenantRef, selection: DeploymentDeleteSelection): Promise<DeploymentDeleteResult>;
|
|
32
34
|
get(deploymentId: string): Promise<ServedTarget | undefined>;
|
|
33
35
|
/** Resolve a deployment-id data-plane route only while that exact record is actively serving. */
|
|
34
36
|
getServing(deploymentId: string): Promise<ServedTarget | undefined>;
|
|
@@ -6,6 +6,7 @@ import { DeploymentPolicyChangedError, UnsupportedDeploymentRecordVersionError,
|
|
|
6
6
|
import { defaultActiveRecord } from './deployment-versioning.js';
|
|
7
7
|
import { updateRegistryAccess } from './registry-access.js';
|
|
8
8
|
import { compilePersistedRegistryRecord, compileRegistryTarget, loadPersistedRegistryTarget, } from './registry-compile.js';
|
|
9
|
+
import { deleteRegistryDeployments, readRegistryTarget, reconcileRegistryTarget, } from './registry-deletion.js';
|
|
9
10
|
import { createRegistryDeployRecord, deployedRecordResult, deploymentWriteVersionError, preflightRegistryDeploy, replacementDeploymentPolicy, resolveDeployAttempt, withDeploymentConfiguration, } from './registry-deploy-transaction.js';
|
|
10
11
|
import { deploymentOwnerSubject } from './registry-helpers.js';
|
|
11
12
|
import { registryDeploymentPackage, renderDeploymentPackageSnapshot } from './registry-package.js';
|
|
@@ -13,7 +14,7 @@ import { recoverRegistryRecords } from './registry-recover.js';
|
|
|
13
14
|
import { rollbackDeployment, rollbackWithMappedErrors } from './registry-rollback.js';
|
|
14
15
|
import { activeRecord, activeRecordVersion, persistDeployRecord, reconcilePlatformAccountResetRegistryCache, registryAppArchivedAt, registryArchiveApp, registryCustomerAuthRestoreProjections, registryGetApp, registryGetDeployment, registryGetEnvironment, registryListApps, registryListEnvironments, registryRestoreApp, registrySetProductionEnvironment, registrySweepArchived, setRegistryDeploymentLock, } from './registry-state.js';
|
|
15
16
|
import { deploymentStatusFor } from './registry-status.js';
|
|
16
|
-
import { deploymentSourceFor, rebindNativeRecords, servedTargetFor,
|
|
17
|
+
import { deploymentSourceFor, rebindNativeRecords, servedTargetFor, tenantDeploymentKey, tenantKey, } from './registry-targets.js';
|
|
17
18
|
import { withBuiltinStateCatalog } from './state-catalog.js';
|
|
18
19
|
import { deploymentSummary, matchesDeploymentFilter, validateDeploymentListFilter, } from './store/records.js';
|
|
19
20
|
import { InMemoryConfigStore, validateTenantRef, } from './store.js';
|
|
@@ -318,26 +319,11 @@ export class ServerRegistry {
|
|
|
318
319
|
#compilePersistedRecord(record) {
|
|
319
320
|
return compilePersistedRegistryRecord(record, this.#compileTarget.bind(this));
|
|
320
321
|
}
|
|
322
|
+
deleteDeployments(ref, selection) {
|
|
323
|
+
return deleteRegistryDeployments(this.#stateView(), ref, selection);
|
|
324
|
+
}
|
|
321
325
|
async get(deploymentId) {
|
|
322
|
-
|
|
323
|
-
return undefined;
|
|
324
|
-
const cached = this.#servers.get(deploymentId);
|
|
325
|
-
if (cached) {
|
|
326
|
-
const record = this.#store
|
|
327
|
-
? await this.#store.get(deploymentId)
|
|
328
|
-
: this.#records.get(deploymentId);
|
|
329
|
-
return record === undefined || record.archivedAt !== undefined
|
|
330
|
-
? undefined
|
|
331
|
-
: this.#targetForPersistedRecord(record);
|
|
332
|
-
}
|
|
333
|
-
const inflight = this.#inflight.get(deploymentId);
|
|
334
|
-
if (inflight)
|
|
335
|
-
return inflight;
|
|
336
|
-
const promise = this.#loadAndCompile(deploymentId).finally(() => {
|
|
337
|
-
this.#inflight.delete(deploymentId);
|
|
338
|
-
});
|
|
339
|
-
this.#inflight.set(deploymentId, promise);
|
|
340
|
-
return promise;
|
|
326
|
+
return readRegistryTarget(this.#stateView(), deploymentId, this.#inflight, (record) => this.#targetForPersistedRecord(record), (id) => this.#loadAndCompile(id));
|
|
341
327
|
}
|
|
342
328
|
/** Resolve a deployment-id data-plane route only while that exact record is actively serving. */
|
|
343
329
|
async getServing(deploymentId) {
|
|
@@ -499,9 +485,7 @@ export class ServerRegistry {
|
|
|
499
485
|
return this.#knowledgeSearch;
|
|
500
486
|
}
|
|
501
487
|
async #targetForPersistedRecord(record) {
|
|
502
|
-
return
|
|
503
|
-
records: this.#records,
|
|
504
|
-
servers: this.#servers,
|
|
488
|
+
return reconcileRegistryTarget(this.#stateView(), record, {
|
|
505
489
|
...(this.#customerVerifierFactory
|
|
506
490
|
? { customerVerifierFactory: this.#customerVerifierFactory }
|
|
507
491
|
: {}),
|
|
@@ -2,9 +2,23 @@ import { applySecurityHeaders, enforceHttps, } from '@noodle-borg/transport-http
|
|
|
2
2
|
import { respondRouteError } from '../http-util.js';
|
|
3
3
|
import { handleDeploy } from './control-plane.js';
|
|
4
4
|
import { handleDeployPreflight } from './deploy-preflight.js';
|
|
5
|
+
import { handleDeploymentDelete, parseVersionDeletePath } from './deployment-delete.js';
|
|
5
6
|
import { handleDeploymentLockUpdate } from './deployment-lock.js';
|
|
6
|
-
import { parseTenantDeploymentLockPath, parseTenantDeployPath, parseTenantDeployPreflightPath, } from './paths.js';
|
|
7
|
+
import { parseDeploymentItemPath, parseTenantDeploymentLockPath, parseTenantDeployPath, parseTenantDeployPreflightPath, } from './paths.js';
|
|
7
8
|
export function dispatchDeployRoutes(req, res, url, deps) {
|
|
9
|
+
if (req.method === 'DELETE') {
|
|
10
|
+
const item = parseDeploymentItemPath(url.pathname);
|
|
11
|
+
const deletion = item
|
|
12
|
+
? { kind: 'deployment', ...item }
|
|
13
|
+
: parseVersionDeletePath(url.pathname);
|
|
14
|
+
if (deletion) {
|
|
15
|
+
applySecurityHeaders(res, deps.tls);
|
|
16
|
+
if (enforceHttps(req, res, deps.tls))
|
|
17
|
+
return true;
|
|
18
|
+
handleDeploymentDelete(req, res, deletion, { ...deps, maxBody: deps.maxBody.control }).catch((error) => respondRouteError(deps.logger, res, 'deployment-delete.error', error));
|
|
19
|
+
return true;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
8
22
|
const deploymentLock = parseTenantDeploymentLockPath(url.pathname);
|
|
9
23
|
if (req.method === 'PATCH' && deploymentLock !== undefined) {
|
|
10
24
|
applySecurityHeaders(res, deps.tls);
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { deleteDeploymentOperation, } from '@noodle-borg/control-plane/portable';
|
|
2
|
+
import { normalizeServerVersion } from '@noodle-borg/module';
|
|
3
|
+
import { readJsonBody, sendJson } from '@noodle-borg/transport-http';
|
|
4
|
+
import { deploymentDeleteResponseSchema, deploymentVersionDeleteRequestSchema, } from '@noodle-borg/wire-contracts';
|
|
5
|
+
import { validateTenantRef } from '../store.js';
|
|
6
|
+
import { authorizeControlPlane } from './control-plane.js';
|
|
7
|
+
/** An exact numeric version, or the legacy unversioned scope; never semantic-version equivalence. */
|
|
8
|
+
export function parseVersionDeletePath(pathname) {
|
|
9
|
+
const match = /^\/v1\/orgs\/([^/]+)\/apps\/([^/]+)\/envs\/([^/]+)\/versions\/([^/]+)$/.exec(pathname);
|
|
10
|
+
if (!match)
|
|
11
|
+
return undefined;
|
|
12
|
+
try {
|
|
13
|
+
const target = validateTenantRef({
|
|
14
|
+
org: decodeURIComponent(match[1]),
|
|
15
|
+
app: decodeURIComponent(match[2]),
|
|
16
|
+
env: decodeURIComponent(match[3]),
|
|
17
|
+
});
|
|
18
|
+
const version = decodeURIComponent(match[4]);
|
|
19
|
+
return {
|
|
20
|
+
kind: 'version',
|
|
21
|
+
org: target.org,
|
|
22
|
+
target,
|
|
23
|
+
...(version === 'legacy' ? {} : { serverVersion: normalizeServerVersion(version) }),
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
return undefined;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
export async function handleDeploymentDelete(req, res, route, deps) {
|
|
31
|
+
const identity = await authorizeControlPlane(req, res, deps.gate, { requireIdentity: true });
|
|
32
|
+
if (identity === false)
|
|
33
|
+
return;
|
|
34
|
+
let expectedDeploymentIds;
|
|
35
|
+
if (route.kind === 'version') {
|
|
36
|
+
const body = await readJsonBody(req, deps.maxBody);
|
|
37
|
+
if (!body.ok)
|
|
38
|
+
return sendJson(res, body.status, { error: body.error });
|
|
39
|
+
const parsed = deploymentVersionDeleteRequestSchema.safeParse(body.value);
|
|
40
|
+
if (!parsed.success)
|
|
41
|
+
return sendJson(res, 400, { error: 'Invalid version deletion confirmation.' });
|
|
42
|
+
expectedDeploymentIds = parsed.data.expectedDeploymentIds;
|
|
43
|
+
}
|
|
44
|
+
const result = await deleteDeploymentOperation(deps, {
|
|
45
|
+
actor: identity,
|
|
46
|
+
scope: route,
|
|
47
|
+
...(expectedDeploymentIds === undefined ? {} : { expectedDeploymentIds }),
|
|
48
|
+
});
|
|
49
|
+
return result.ok
|
|
50
|
+
? sendJson(res, 200, deploymentDeleteResponseSchema.parse(result.view))
|
|
51
|
+
: sendJson(res, result.status, { code: result.code, error: result.error });
|
|
52
|
+
}
|
|
53
|
+
//# sourceMappingURL=deployment-delete.js.map
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import type { ActiveAccessUpdateInput, AppArchiveResult, AppRestorePrecondition, AppRestoreResult, AppSummary, ArtifactStore, DeploymentActivationPrecondition, DeploymentActivationResult, DeploymentListFilter, DeploymentLock, DeploymentLockUpdateResult, DeploymentPolicyPrecondition, DeploymentSummary, DeployRecord, EnvSummary, ProductionEnvironmentChange, TenantAuthConfig, TenantRef } from '../store.js';
|
|
1
|
+
import type { ActiveAccessUpdateInput, AppArchiveResult, AppRestorePrecondition, AppRestoreResult, AppSummary, ArtifactStore, DeploymentActivationPrecondition, DeploymentActivationResult, DeploymentDeleteResult, DeploymentDeleteSelection, DeploymentListFilter, DeploymentLock, DeploymentLockUpdateResult, DeploymentPolicyPrecondition, DeploymentSummary, DeployRecord, EnvSummary, ProductionEnvironmentChange, TenantAuthConfig, TenantRef } from '../store.js';
|
|
2
2
|
/** In-memory store: keeps records in a Map. Used by tests and as an explicit, non-persistent option. */
|
|
3
3
|
export declare class InMemoryArtifactStore implements ArtifactStore {
|
|
4
4
|
#private;
|
|
5
|
+
deleteDeployments(ref: TenantRef, selection: DeploymentDeleteSelection): Promise<DeploymentDeleteResult>;
|
|
5
6
|
append(record: DeployRecord, precondition?: DeploymentPolicyPrecondition): Promise<void>;
|
|
6
7
|
loadAll(): Promise<readonly DeployRecord[]>;
|
|
7
8
|
get(deploymentId: string): Promise<DeployRecord | undefined>;
|
|
@@ -2,6 +2,7 @@ import { normalizeServerVersion } from '@noodle-borg/module';
|
|
|
2
2
|
import { assertSameAppPackageSnapshot, sanitizeDeployRecordAppPackageSnapshot, } from '../app-package-snapshot.js';
|
|
3
3
|
import { assertCustomerAuthRestorePrecondition, assertUniqueActiveCustomerAuthAudienceBindings, findActiveCustomerAuthAudienceConflict, requiresCustomerAuthProjection, } from '../customer-auth-audience-binding.js';
|
|
4
4
|
import { matchesDeploymentActivation } from '../deployment-activation-precondition.js';
|
|
5
|
+
import { planDeploymentDeletion } from '../deployment-deletion.js';
|
|
5
6
|
import { assertDeploymentActivationUnlocked, assertDeploymentAppendUnlocked, } from '../deployment-lock.js';
|
|
6
7
|
import { assertDeploymentAppendPolicy, assertDeploymentAppendVersion, } from '../deployment-record-version.js';
|
|
7
8
|
import { defaultActiveRecord, sameDeploymentScope, sameTenantRecord, } from '../deployment-versioning.js';
|
|
@@ -11,6 +12,13 @@ import { validateSlug, validateTenantRef } from './validate.js';
|
|
|
11
12
|
export class InMemoryArtifactStore {
|
|
12
13
|
#records = new Map();
|
|
13
14
|
#productionEnvironments = new Map();
|
|
15
|
+
deleteDeployments(ref, selection) {
|
|
16
|
+
const result = planDeploymentDeletion([...this.#records.values()], ref, selection);
|
|
17
|
+
if (result.ok)
|
|
18
|
+
for (const record of result.deleted)
|
|
19
|
+
this.#records.delete(record.deploymentId);
|
|
20
|
+
return Promise.resolve(result);
|
|
21
|
+
}
|
|
14
22
|
async append(record, precondition) {
|
|
15
23
|
assertDeploymentAppendVersion([...this.#records.values()], record);
|
|
16
24
|
assertDeploymentAppendPolicy([...this.#records.values()], record, precondition);
|