@tiangong-lca/cli 0.0.28 → 0.0.30

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 (38) hide show
  1. package/README.md +90 -2
  2. package/dist/src/cli.js +553 -4
  3. package/dist/src/cli.js.map +1 -1
  4. package/dist/src/lib/dataset-command.js +11 -0
  5. package/dist/src/lib/dataset-command.js.map +1 -1
  6. package/dist/src/lib/dataset-maintenance-contract.js.map +1 -1
  7. package/dist/src/lib/dataset-maintenance-flow-identity-approval-claim.js +175 -0
  8. package/dist/src/lib/dataset-maintenance-flow-identity-approval-claim.js.map +1 -0
  9. package/dist/src/lib/dataset-maintenance-flow-identity-capture.js +511 -0
  10. package/dist/src/lib/dataset-maintenance-flow-identity-capture.js.map +1 -0
  11. package/dist/src/lib/dataset-maintenance-flow-identity-command.js +26 -0
  12. package/dist/src/lib/dataset-maintenance-flow-identity-command.js.map +1 -0
  13. package/dist/src/lib/dataset-maintenance-flow-identity-contract.js +784 -0
  14. package/dist/src/lib/dataset-maintenance-flow-identity-contract.js.map +1 -0
  15. package/dist/src/lib/dataset-maintenance-flow-identity-execution-contract.js +1317 -0
  16. package/dist/src/lib/dataset-maintenance-flow-identity-execution-contract.js.map +1 -0
  17. package/dist/src/lib/dataset-maintenance-flow-identity-freeze.js +342 -0
  18. package/dist/src/lib/dataset-maintenance-flow-identity-freeze.js.map +1 -0
  19. package/dist/src/lib/dataset-maintenance-flow-identity-plan.js +900 -0
  20. package/dist/src/lib/dataset-maintenance-flow-identity-plan.js.map +1 -0
  21. package/dist/src/lib/dataset-maintenance-flow-identity-recovery.js +688 -0
  22. package/dist/src/lib/dataset-maintenance-flow-identity-recovery.js.map +1 -0
  23. package/dist/src/lib/dataset-maintenance-flow-identity-run.js +1369 -0
  24. package/dist/src/lib/dataset-maintenance-flow-identity-run.js.map +1 -0
  25. package/dist/src/lib/dataset-maintenance-flow-identity-seal.js +144 -0
  26. package/dist/src/lib/dataset-maintenance-flow-identity-seal.js.map +1 -0
  27. package/dist/src/lib/dataset-maintenance-flow-identity-verify.js +377 -0
  28. package/dist/src/lib/dataset-maintenance-flow-identity-verify.js.map +1 -0
  29. package/dist/src/lib/dataset-maintenance-flow-identity-wire.js +178 -0
  30. package/dist/src/lib/dataset-maintenance-flow-identity-wire.js.map +1 -0
  31. package/dist/src/lib/dataset-maintenance-remote.js +156 -10
  32. package/dist/src/lib/dataset-maintenance-remote.js.map +1 -1
  33. package/dist/src/lib/dataset-save-draft-run.js +667 -0
  34. package/dist/src/lib/dataset-save-draft-run.js.map +1 -1
  35. package/dist/src/lib/http.js.map +1 -1
  36. package/dist/src/lib/lca-release.js +683 -0
  37. package/dist/src/lib/lca-release.js.map +1 -0
  38. package/package.json +1 -1
@@ -0,0 +1,175 @@
1
+ import { closeSync, existsSync, fsyncSync, lstatSync, mkdirSync, openSync, readFileSync, writeFileSync, } from 'node:fs';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
4
+ import { stableJsonText } from './dataset-maintenance-contract.js';
5
+ import { CliError } from './errors.js';
6
+ const HASH = /^[a-f0-9]{64}$/u;
7
+ const UUID = /^[a-f0-9]{8}-[a-f0-9]{4}-[1-5][a-f0-9]{3}-[89ab][a-f0-9]{3}-[a-f0-9]{12}$/u;
8
+ const EMAIL = /^[^\s@]+@[^\s@]+$/u;
9
+ const CLAIM_KEYS = [
10
+ 'schema_version',
11
+ 'claimed_at_utc',
12
+ 'approval_kind',
13
+ 'approval_identity_sha256',
14
+ 'execution_identity_sha256',
15
+ 'request_id',
16
+ 'environment',
17
+ 'project_ref',
18
+ 'actor_user_id',
19
+ 'actor_email',
20
+ 'target_visibility',
21
+ 'user_state_claim',
22
+ 'plan_sha256',
23
+ 'freeze_sha256',
24
+ 'canonical_out_dir',
25
+ 'maximum_cli_apply_spawns',
26
+ 'approval_reusable',
27
+ ];
28
+ function fail(message, code, details) {
29
+ throw new CliError(message, { code, exitCode: 1, details });
30
+ }
31
+ function isErrno(error, code) {
32
+ return Boolean(error &&
33
+ typeof error === 'object' &&
34
+ 'code' in error &&
35
+ error.code === code);
36
+ }
37
+ function hasPrivateClaimPermissions(mode, platform) {
38
+ // Windows does not expose its ACLs through POSIX permission bits. The claim
39
+ // still has to be a non-symlink regular file and is created exclusively below
40
+ // the current user's state directory; enforce 0600-style bits where they are
41
+ // meaningful.
42
+ return platform === 'win32' || (mode & 0o077) === 0;
43
+ }
44
+ function rethrowClaimCreateError(error, claimPath) {
45
+ if (isErrno(error, 'EEXIST')) {
46
+ fail('This exact flow identity approval was already claimed by a CLI wrapper. Only read-only status or a newly frozen recovery approval is allowed.', 'DATASET_FLOW_IDENTITY_APPROVAL_ALREADY_CLAIMED', { claim_path: claimPath });
47
+ }
48
+ throw error;
49
+ }
50
+ function validateClaim(value, expectedIdentity) {
51
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
52
+ fail('Existing flow identity approval claim is malformed or foreign.', 'DATASET_FLOW_IDENTITY_APPROVAL_CLAIM_INVALID');
53
+ }
54
+ const claim = value;
55
+ const actualKeys = Object.keys(claim).sort();
56
+ const expectedKeys = [...CLAIM_KEYS].sort();
57
+ const claimedAt = String(claim.claimed_at_utc ?? '');
58
+ const canonicalOutDir = String(claim.canonical_out_dir ?? '');
59
+ if (actualKeys.length !== expectedKeys.length ||
60
+ actualKeys.some((key, index) => key !== expectedKeys[index]) ||
61
+ claim.schema_version !== 'dataset-flow-identity-local-approval-claim.v1' ||
62
+ !Number.isFinite(Date.parse(claimedAt)) ||
63
+ new Date(claimedAt).toISOString() !== claimedAt ||
64
+ !['initial', 'recovery'].includes(String(claim.approval_kind)) ||
65
+ claim.approval_identity_sha256 !== expectedIdentity ||
66
+ ![
67
+ claim.approval_identity_sha256,
68
+ claim.execution_identity_sha256,
69
+ claim.plan_sha256,
70
+ claim.freeze_sha256,
71
+ ].every((entry) => typeof entry === 'string' && HASH.test(entry)) ||
72
+ typeof claim.request_id !== 'string' ||
73
+ !UUID.test(claim.request_id) ||
74
+ claim.environment !== 'production' ||
75
+ typeof claim.project_ref !== 'string' ||
76
+ !claim.project_ref.trim() ||
77
+ typeof claim.actor_user_id !== 'string' ||
78
+ !UUID.test(claim.actor_user_id) ||
79
+ typeof claim.actor_email !== 'string' ||
80
+ !EMAIL.test(claim.actor_email) ||
81
+ claim.actor_email !== claim.actor_email.trim().toLowerCase() ||
82
+ claim.target_visibility !== 'owner_draft' ||
83
+ claim.user_state_claim !== 'authenticated_actor_state_100_plus_own_state_0' ||
84
+ !path.isAbsolute(canonicalOutDir) ||
85
+ path.resolve(canonicalOutDir) !== canonicalOutDir ||
86
+ claim.maximum_cli_apply_spawns !== 1 ||
87
+ claim.approval_reusable !== false) {
88
+ fail('Existing flow identity approval claim is malformed or foreign.', 'DATASET_FLOW_IDENTITY_APPROVAL_CLAIM_INVALID');
89
+ }
90
+ return claim;
91
+ }
92
+ export function resolveFlowIdentityApprovalClaimRoot(options) {
93
+ const platform = options.platform ?? process.platform;
94
+ const homeDir = options.homeDir ?? os.homedir();
95
+ const xdg = options.env.XDG_STATE_HOME?.trim();
96
+ if (xdg)
97
+ return path.resolve(xdg, 'tiangong-lca-cli');
98
+ if (platform === 'win32') {
99
+ const localAppData = options.env.LOCALAPPDATA?.trim();
100
+ if (localAppData)
101
+ return path.resolve(localAppData, 'tiangong-lca-cli');
102
+ }
103
+ if (homeDir.trim()) {
104
+ return path.resolve(homeDir, '.local', 'state', 'tiangong-lca-cli');
105
+ }
106
+ if (platform === 'darwin') {
107
+ return path.resolve(os.homedir(), 'Library', 'Application Support', 'tiangong-lca-cli');
108
+ }
109
+ return path.resolve('.tiangong-lca-cli-state');
110
+ }
111
+ export function flowIdentityApprovalClaimPath(options) {
112
+ if (!HASH.test(options.approvalIdentitySha256)) {
113
+ fail('Flow identity approval claim requires a lowercase approval identity SHA-256.', 'DATASET_FLOW_IDENTITY_APPROVAL_CLAIM_IDENTITY_INVALID');
114
+ }
115
+ return path.join(path.resolve(options.stateRoot), 'execution-approvals', 'flow-identity', 'v3', `${options.approvalIdentitySha256}.claim.json`);
116
+ }
117
+ /** Strict create-only claim. Existing identical bytes are still consumed. */
118
+ export function claimFlowIdentityApproval(options) {
119
+ validateClaim(options.claim, options.claim.approval_identity_sha256);
120
+ const claimPath = flowIdentityApprovalClaimPath({
121
+ stateRoot: options.stateRoot ?? resolveFlowIdentityApprovalClaimRoot({ env: options.env }),
122
+ approvalIdentitySha256: options.claim.approval_identity_sha256,
123
+ });
124
+ const directory = path.dirname(claimPath);
125
+ mkdirSync(directory, { recursive: true, mode: 0o700 });
126
+ const bytes = `${stableJsonText(options.claim)}\n`;
127
+ let descriptor = null;
128
+ try {
129
+ descriptor = openSync(claimPath, 'wx', 0o600);
130
+ writeFileSync(descriptor, bytes, 'utf8');
131
+ fsyncSync(descriptor);
132
+ }
133
+ catch (error) {
134
+ rethrowClaimCreateError(error, claimPath);
135
+ }
136
+ finally {
137
+ if (descriptor !== null)
138
+ closeSync(descriptor);
139
+ }
140
+ return claimPath;
141
+ }
142
+ export function readFlowIdentityApprovalClaim(options) {
143
+ const claimPath = flowIdentityApprovalClaimPath({
144
+ stateRoot: options.stateRoot ?? resolveFlowIdentityApprovalClaimRoot({ env: options.env }),
145
+ approvalIdentitySha256: options.approvalIdentitySha256,
146
+ });
147
+ if (!existsSync(claimPath))
148
+ return null;
149
+ const metadata = lstatSync(claimPath);
150
+ if (!metadata.isFile() ||
151
+ metadata.isSymbolicLink() ||
152
+ !hasPrivateClaimPermissions(metadata.mode, process.platform)) {
153
+ return fail('Existing flow identity approval claim is not a private regular file.', 'DATASET_FLOW_IDENTITY_APPROVAL_CLAIM_INVALID', { claim_path: claimPath });
154
+ }
155
+ let text;
156
+ let value;
157
+ try {
158
+ text = readFileSync(claimPath, 'utf8');
159
+ value = JSON.parse(text);
160
+ }
161
+ catch {
162
+ return fail('Existing flow identity approval claim is unreadable.', 'DATASET_FLOW_IDENTITY_APPROVAL_CLAIM_INVALID', { claim_path: claimPath });
163
+ }
164
+ if (text !== `${stableJsonText(value)}\n`) {
165
+ return fail('Existing flow identity approval claim is not canonical JSON.', 'DATASET_FLOW_IDENTITY_APPROVAL_CLAIM_INVALID', { claim_path: claimPath });
166
+ }
167
+ return validateClaim(value, options.approvalIdentitySha256);
168
+ }
169
+ export const __testInternals = {
170
+ hasPrivateClaimPermissions,
171
+ isErrno,
172
+ rethrowClaimCreateError,
173
+ validateClaim,
174
+ };
175
+ //# sourceMappingURL=dataset-maintenance-flow-identity-approval-claim.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dataset-maintenance-flow-identity-approval-claim.js","sourceRoot":"","sources":["../../../src/lib/dataset-maintenance-flow-identity-approval-claim.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n closeSync,\n existsSync,\n fsyncSync,\n lstatSync,\n mkdirSync,\n openSync,\n readFileSync,\n writeFileSync,\n} from 'node:fs';\nimport os from 'node:os';\nimport path from 'node:path';\nimport { stableJsonText } from './dataset-maintenance-contract.js';\nimport { CliError } from './errors.js';\n\nconst HASH = /^[a-f0-9]{64}$/u;\nconst UUID = /^[a-f0-9]{8}-[a-f0-9]{4}-[1-5][a-f0-9]{3}-[89ab][a-f0-9]{3}-[a-f0-9]{12}$/u;\nconst EMAIL = /^[^\\s@]+@[^\\s@]+$/u;\nconst CLAIM_KEYS = [\n 'schema_version',\n 'claimed_at_utc',\n 'approval_kind',\n 'approval_identity_sha256',\n 'execution_identity_sha256',\n 'request_id',\n 'environment',\n 'project_ref',\n 'actor_user_id',\n 'actor_email',\n 'target_visibility',\n 'user_state_claim',\n 'plan_sha256',\n 'freeze_sha256',\n 'canonical_out_dir',\n 'maximum_cli_apply_spawns',\n 'approval_reusable',\n] as const;\n\nexport type FlowIdentityApprovalClaim = {\n schema_version: 'dataset-flow-identity-local-approval-claim.v1';\n claimed_at_utc: string;\n approval_kind: 'initial' | 'recovery';\n approval_identity_sha256: string;\n execution_identity_sha256: string;\n request_id: string;\n environment: 'production';\n project_ref: string;\n actor_user_id: string;\n actor_email: string;\n target_visibility: 'owner_draft';\n user_state_claim: 'authenticated_actor_state_100_plus_own_state_0';\n plan_sha256: string;\n freeze_sha256: string;\n canonical_out_dir: string;\n maximum_cli_apply_spawns: 1;\n approval_reusable: false;\n};\n\nfunction fail(message: string, code: string, details?: unknown): never {\n throw new CliError(message, { code, exitCode: 1, details });\n}\n\nfunction isErrno(error: unknown, code: string): boolean {\n return Boolean(\n error &&\n typeof error === 'object' &&\n 'code' in error &&\n (error as NodeJS.ErrnoException).code === code,\n );\n}\n\nfunction hasPrivateClaimPermissions(mode: number, platform: NodeJS.Platform): boolean {\n // Windows does not expose its ACLs through POSIX permission bits. The claim\n // still has to be a non-symlink regular file and is created exclusively below\n // the current user's state directory; enforce 0600-style bits where they are\n // meaningful.\n return platform === 'win32' || (mode & 0o077) === 0;\n}\n\nfunction rethrowClaimCreateError(error: unknown, claimPath: string): never {\n if (isErrno(error, 'EEXIST')) {\n fail(\n 'This exact flow identity approval was already claimed by a CLI wrapper. Only read-only status or a newly frozen recovery approval is allowed.',\n 'DATASET_FLOW_IDENTITY_APPROVAL_ALREADY_CLAIMED',\n { claim_path: claimPath },\n );\n }\n throw error;\n}\n\nfunction validateClaim(value: unknown, expectedIdentity: string): FlowIdentityApprovalClaim {\n if (!value || typeof value !== 'object' || Array.isArray(value)) {\n fail(\n 'Existing flow identity approval claim is malformed or foreign.',\n 'DATASET_FLOW_IDENTITY_APPROVAL_CLAIM_INVALID',\n );\n }\n const claim = value as Partial<FlowIdentityApprovalClaim>;\n const actualKeys = Object.keys(claim).sort();\n const expectedKeys = [...CLAIM_KEYS].sort();\n const claimedAt = String(claim.claimed_at_utc ?? '');\n const canonicalOutDir = String(claim.canonical_out_dir ?? '');\n if (\n actualKeys.length !== expectedKeys.length ||\n actualKeys.some((key, index) => key !== expectedKeys[index]) ||\n claim.schema_version !== 'dataset-flow-identity-local-approval-claim.v1' ||\n !Number.isFinite(Date.parse(claimedAt)) ||\n new Date(claimedAt).toISOString() !== claimedAt ||\n !['initial', 'recovery'].includes(String(claim.approval_kind)) ||\n claim.approval_identity_sha256 !== expectedIdentity ||\n ![\n claim.approval_identity_sha256,\n claim.execution_identity_sha256,\n claim.plan_sha256,\n claim.freeze_sha256,\n ].every((entry) => typeof entry === 'string' && HASH.test(entry)) ||\n typeof claim.request_id !== 'string' ||\n !UUID.test(claim.request_id) ||\n claim.environment !== 'production' ||\n typeof claim.project_ref !== 'string' ||\n !claim.project_ref.trim() ||\n typeof claim.actor_user_id !== 'string' ||\n !UUID.test(claim.actor_user_id) ||\n typeof claim.actor_email !== 'string' ||\n !EMAIL.test(claim.actor_email) ||\n claim.actor_email !== claim.actor_email.trim().toLowerCase() ||\n claim.target_visibility !== 'owner_draft' ||\n claim.user_state_claim !== 'authenticated_actor_state_100_plus_own_state_0' ||\n !path.isAbsolute(canonicalOutDir) ||\n path.resolve(canonicalOutDir) !== canonicalOutDir ||\n claim.maximum_cli_apply_spawns !== 1 ||\n claim.approval_reusable !== false\n ) {\n fail(\n 'Existing flow identity approval claim is malformed or foreign.',\n 'DATASET_FLOW_IDENTITY_APPROVAL_CLAIM_INVALID',\n );\n }\n return claim as FlowIdentityApprovalClaim;\n}\n\nexport function resolveFlowIdentityApprovalClaimRoot(options: {\n env: NodeJS.ProcessEnv;\n platform?: NodeJS.Platform;\n homeDir?: string;\n}): string {\n const platform = options.platform ?? process.platform;\n const homeDir = options.homeDir ?? os.homedir();\n const xdg = options.env.XDG_STATE_HOME?.trim();\n if (xdg) return path.resolve(xdg, 'tiangong-lca-cli');\n if (platform === 'win32') {\n const localAppData = options.env.LOCALAPPDATA?.trim();\n if (localAppData) return path.resolve(localAppData, 'tiangong-lca-cli');\n }\n if (homeDir.trim()) {\n return path.resolve(homeDir, '.local', 'state', 'tiangong-lca-cli');\n }\n if (platform === 'darwin') {\n return path.resolve(os.homedir(), 'Library', 'Application Support', 'tiangong-lca-cli');\n }\n return path.resolve('.tiangong-lca-cli-state');\n}\n\nexport function flowIdentityApprovalClaimPath(options: {\n stateRoot: string;\n approvalIdentitySha256: string;\n}): string {\n if (!HASH.test(options.approvalIdentitySha256)) {\n fail(\n 'Flow identity approval claim requires a lowercase approval identity SHA-256.',\n 'DATASET_FLOW_IDENTITY_APPROVAL_CLAIM_IDENTITY_INVALID',\n );\n }\n return path.join(\n path.resolve(options.stateRoot),\n 'execution-approvals',\n 'flow-identity',\n 'v3',\n `${options.approvalIdentitySha256}.claim.json`,\n );\n}\n\n/** Strict create-only claim. Existing identical bytes are still consumed. */\nexport function claimFlowIdentityApproval(options: {\n claim: FlowIdentityApprovalClaim;\n env: NodeJS.ProcessEnv;\n stateRoot?: string;\n}): string {\n validateClaim(options.claim, options.claim.approval_identity_sha256);\n const claimPath = flowIdentityApprovalClaimPath({\n stateRoot: options.stateRoot ?? resolveFlowIdentityApprovalClaimRoot({ env: options.env }),\n approvalIdentitySha256: options.claim.approval_identity_sha256,\n });\n const directory = path.dirname(claimPath);\n mkdirSync(directory, { recursive: true, mode: 0o700 });\n const bytes = `${stableJsonText(options.claim)}\\n`;\n let descriptor: number | null = null;\n try {\n descriptor = openSync(claimPath, 'wx', 0o600);\n writeFileSync(descriptor, bytes, 'utf8');\n fsyncSync(descriptor);\n } catch (error) {\n rethrowClaimCreateError(error, claimPath);\n } finally {\n if (descriptor !== null) closeSync(descriptor);\n }\n return claimPath;\n}\n\nexport function readFlowIdentityApprovalClaim(options: {\n approvalIdentitySha256: string;\n env: NodeJS.ProcessEnv;\n stateRoot?: string;\n}): FlowIdentityApprovalClaim | null {\n const claimPath = flowIdentityApprovalClaimPath({\n stateRoot: options.stateRoot ?? resolveFlowIdentityApprovalClaimRoot({ env: options.env }),\n approvalIdentitySha256: options.approvalIdentitySha256,\n });\n if (!existsSync(claimPath)) return null;\n const metadata = lstatSync(claimPath);\n if (\n !metadata.isFile() ||\n metadata.isSymbolicLink() ||\n !hasPrivateClaimPermissions(metadata.mode, process.platform)\n ) {\n return fail(\n 'Existing flow identity approval claim is not a private regular file.',\n 'DATASET_FLOW_IDENTITY_APPROVAL_CLAIM_INVALID',\n { claim_path: claimPath },\n );\n }\n let text: string;\n let value: unknown;\n try {\n text = readFileSync(claimPath, 'utf8');\n value = JSON.parse(text);\n } catch {\n return fail(\n 'Existing flow identity approval claim is unreadable.',\n 'DATASET_FLOW_IDENTITY_APPROVAL_CLAIM_INVALID',\n { claim_path: claimPath },\n );\n }\n if (text !== `${stableJsonText(value)}\\n`) {\n return fail(\n 'Existing flow identity approval claim is not canonical JSON.',\n 'DATASET_FLOW_IDENTITY_APPROVAL_CLAIM_INVALID',\n { claim_path: claimPath },\n );\n }\n return validateClaim(value, options.approvalIdentitySha256);\n}\n\nexport const __testInternals = {\n hasPrivateClaimPermissions,\n isErrno,\n rethrowClaimCreateError,\n validateClaim,\n};\n"]}