openyida 2026.7.19 → 2026.7.21
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/README.md +14 -0
- package/bin/yida.js +107 -18
- package/lib/app/create-app.js +12 -1
- package/lib/app/create-form/args.js +13 -0
- package/lib/app/create-form.js +154 -67
- package/lib/app/create-page.js +45 -9
- package/lib/app/get-schema.js +65 -14
- package/lib/app/import-app.js +92 -41
- package/lib/app/publish.js +289 -556
- package/lib/app/schema-field-resolution.js +370 -0
- package/lib/app/services/app-reader.js +85 -0
- package/lib/app/services/app-service.js +235 -0
- package/lib/app/services/canvas-page-compiler.js +91 -0
- package/lib/app/services/canvas-page-schema-builder.js +149 -0
- package/lib/app/services/field-bindings.js +68 -0
- package/lib/app/services/form-compiler.js +1922 -0
- package/lib/app/services/form-mode-service.js +182 -0
- package/lib/app/services/form-schema-patcher.js +333 -0
- package/lib/app/services/form-schema-reader.js +59 -0
- package/lib/app/services/form-service.js +379 -0
- package/lib/app/services/form-validation.js +562 -0
- package/lib/app/services/native-page-compiler.js +92 -0
- package/lib/app/services/native-page-schema-builder.js +428 -0
- package/lib/app/services/page-resource-service.js +185 -0
- package/lib/app/update-app.js +18 -6
- package/lib/auth/token-auth.js +34 -3
- package/lib/auth/token-store.js +22 -4
- package/lib/bridge/bridge.js +37 -5
- package/lib/core/agent-capabilities.js +5 -3
- package/lib/core/command-manifest.js +40 -0
- package/lib/core/doctor.js +15 -4
- package/lib/core/env-cmd.js +1 -1
- package/lib/core/env.js +7 -6
- package/lib/core/legacy-schema-guard.js +408 -0
- package/lib/core/locales/en.js +8 -46
- package/lib/core/locales/zh.js +8 -46
- package/lib/core/sample.js +1 -1
- package/lib/core/utils.js +334 -66
- package/lib/core/yida-client.js +104 -3
- package/lib/formula/evaluate.js +8 -66
- package/lib/formula/field-refs.js +83 -0
- package/lib/process/configure-process.js +44 -1992
- package/lib/process/create-process.js +54 -87
- package/lib/process/services/process-compiler.js +2056 -0
- package/lib/process/services/process-reader.js +232 -0
- package/lib/process/services/process-resource-service.js +571 -0
- package/lib/process/services/process-service.js +122 -0
- package/lib/process/services/process-stage-checkpoint.js +246 -0
- package/lib/report/append.js +7 -26
- package/lib/report/http.js +31 -5
- package/lib/report/index.js +10 -2
- package/lib/samples/yida-canvas-custom-page/business-list.canvas.jsx +63 -14
- package/lib/samples/yida-canvas-custom-page/dashboard-overview.canvas.jsx +176 -32
- package/lib/samples/yida-canvas-custom-page/data-management.canvas.jsx +228 -14
- package/lib/samples/yida-canvas-custom-page/split-pane-detail.canvas.jsx +204 -8
- package/lib/schema/adapters/app-adapter.js +210 -0
- package/lib/schema/adapters/form-adapter.js +1362 -0
- package/lib/schema/adapters/keys.js +19 -0
- package/lib/schema/adapters/page-adapter.js +566 -0
- package/lib/schema/adapters/process-adapter.js +519 -0
- package/lib/schema/applier.js +1878 -0
- package/lib/schema/apply-store.js +1083 -0
- package/lib/schema/command.js +645 -0
- package/lib/schema/dependency-graph.js +83 -0
- package/lib/schema/errors.js +481 -0
- package/lib/schema/hash.js +9 -0
- package/lib/schema/manifest-limits.js +177 -0
- package/lib/schema/manifest-loader.js +309 -0
- package/lib/schema/manifest-schema-v1.json +193 -0
- package/lib/schema/normalize-manifest.js +281 -0
- package/lib/schema/page-canvas-foundation.js +447 -0
- package/lib/schema/page-data-source-builder.js +561 -0
- package/lib/schema/page-foundation.js +556 -0
- package/lib/schema/page-source-loader.js +314 -0
- package/lib/schema/planner.js +691 -0
- package/lib/schema/remote-dispatch-boundary.js +39 -0
- package/lib/schema/remote-missing.js +21 -0
- package/lib/schema/remote-reader.js +189 -0
- package/lib/schema/resource-registry.js +157 -0
- package/lib/schema/server-revision.js +71 -0
- package/lib/schema/sort.js +28 -0
- package/lib/schema/state-store.js +589 -0
- package/package.json +2 -2
- package/scripts/postinstall.js +8 -12
- package/scripts/validate-package-size.js +1 -1
- package/yida-skills/SKILL.md +114 -29
- package/yida-skills/references/schema-as-code-phase1.md +99 -0
- package/yida-skills/references/setup-and-env.md +59 -59
- package/yida-skills/references/task-retrospective.md +6 -6
- package/yida-skills/skills/yida-app/SKILL.md +60 -14
- package/yida-skills/skills/yida-canvas-custom-page/SKILL.md +18 -7
- package/yida-skills/skills/yida-canvas-custom-page/references/data-bridge-guide.md +3 -3
- package/yida-skills/skills/yida-canvas-custom-page/references/page-generation-guide.md +7 -1
- package/yida-skills/skills/yida-create-app/SKILL.md +21 -3
- package/yida-skills/skills/yida-create-form-page/SKILL.md +19 -1
- package/yida-skills/skills/yida-create-page/SKILL.md +16 -3
- package/yida-skills/skills/yida-create-process/SKILL.md +100 -4
- package/yida-skills/skills/yida-custom-page/SKILL.md +19 -4
- package/yida-skills/skills/yida-dashboard/SKILL.md +1 -1
- package/yida-skills/skills/yida-dashboard/references/pitfalls.md +1 -1
- package/yida-skills/skills/yida-get-schema/SKILL.md +49 -5
- package/yida-skills/skills/yida-i18n/SKILL.md +1 -1
- package/yida-skills/skills/yida-integration/SKILL.md +7 -1
- package/yida-skills/skills/yida-login/SKILL.md +50 -99
- package/yida-skills/skills/yida-page-config/SKILL.md +7 -1
- package/yida-skills/skills/yida-page-uiux/references/app/blueprint.md +0 -1
- package/yida-skills/skills/yida-page-uiux/references/app/navigation-patterns.md +0 -1
- package/yida-skills/skills/yida-page-uiux/references/app/role-journey.md +0 -1
- package/yida-skills/skills/yida-page-uiux/references/dashboard/app-blueprint.md +0 -1
- package/yida-skills/skills/yida-page-uiux/references/dashboard/chart-contracts.md +0 -1
- package/yida-skills/skills/yida-page-uiux/references/dashboard/component-contracts.md +0 -1
- package/yida-skills/skills/yida-page-uiux/references/dashboard/layout-patterns.md +0 -1
- package/yida-skills/skills/yida-page-uiux/references/dashboard/theme-recipes.md +0 -1
- package/yida-skills/skills/yida-page-uiux/references/detail/object-narrative.md +0 -1
- package/yida-skills/skills/yida-page-uiux/references/detail/section-patterns.md +0 -1
- package/yida-skills/skills/yida-page-uiux/references/detail/timeline-and-related.md +0 -1
- package/yida-skills/skills/yida-page-uiux/references/landing/assets-workflow.md +0 -1
- package/yida-skills/skills/yida-page-uiux/references/landing/industry-playbooks.md +0 -1
- package/yida-skills/skills/yida-page-uiux/references/landing/research-levels.md +0 -1
- package/yida-skills/skills/yida-page-uiux/references/landing/section-patterns.md +0 -1
- package/yida-skills/skills/yida-page-uiux/references/list/drawer-detail.md +0 -1
- package/yida-skills/skills/yida-page-uiux/references/list/empty-loading-error.md +0 -1
- package/yida-skills/skills/yida-page-uiux/references/list/filter-patterns.md +0 -1
- package/yida-skills/skills/yida-page-uiux/references/list/table-patterns.md +0 -1
- package/yida-skills/skills/yida-page-uiux/references/scenes/screen.md +0 -1
- package/yida-skills/skills/yida-page-uiux/references/workbench/entry-patterns.md +0 -1
- package/yida-skills/skills/yida-page-uiux/references/workbench/portal-layouts.md +0 -1
- package/yida-skills/skills/yida-page-uiux/references/workbench/task-feed.md +0 -1
- package/yida-skills/skills/yida-page-uiux/workflow/output-decision-block.md +1 -1
- package/yida-skills/skills/yida-process-rule/SKILL.md +35 -1
- package/yida-skills/skills/yida-publish-page/SKILL.md +46 -4
- package/yida-skills/skills/yida-report/SKILL.md +7 -1
- package/yida-skills/skills-index.json +14 -46
- package/yida-skills/skills/large-file-write/SKILL.md +0 -93
- package/yida-skills/skills/large-file-write/references/write-patterns.md +0 -147
- package/yida-skills/skills/large-file-write/scripts/write.js +0 -157
- package/yida-skills/skills/sls-log-workbench/SKILL.md +0 -134
- package/yida-skills/skills/sls-log-workbench/references/workbench-reference.md +0 -130
- package/yida-skills/skills/sls-log-workbench/sls-query.js +0 -295
|
@@ -0,0 +1,1083 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const crypto = require('crypto');
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
const { schemaError } = require('./errors');
|
|
7
|
+
const { stableStringify } = require('./normalize-manifest');
|
|
8
|
+
const { createDefaultRegistry, isStagedAdapter } = require('./resource-registry');
|
|
9
|
+
const {
|
|
10
|
+
createEmptyState,
|
|
11
|
+
createEnvironmentIdentity,
|
|
12
|
+
hashStable,
|
|
13
|
+
getResourceState,
|
|
14
|
+
upsertResourceState,
|
|
15
|
+
} = require('./state-store');
|
|
16
|
+
|
|
17
|
+
const APPLY_JOURNAL_KIND = 'openyida_schema_apply_operations';
|
|
18
|
+
const APPLY_STORE_CONTRACT_VERSION = 1;
|
|
19
|
+
const APPLY_LOCK_KIND = 'openyida_schema_apply_lock';
|
|
20
|
+
const APPLY_LOCK_CONTRACT_VERSION = 1;
|
|
21
|
+
const HASH_PATTERN = /^sha256:[a-f0-9]{64}$/;
|
|
22
|
+
const ATOMIC_NONCE_PATTERN = /^[a-f0-9]{24}$/;
|
|
23
|
+
const OWNER_TOKEN_PATTERN = /^[a-f0-9]{32}$/;
|
|
24
|
+
const OPERATION_STATUSES = new Set(['pending', 'completed', 'uncertain']);
|
|
25
|
+
const OPERATION_TYPES = new Set(['create', 'update', 'stateRepair']);
|
|
26
|
+
const APPLY_LOCK_HANDLES = new WeakMap();
|
|
27
|
+
|
|
28
|
+
function resolveApplyPaths(statePath) {
|
|
29
|
+
const directory = path.dirname(statePath);
|
|
30
|
+
return {
|
|
31
|
+
lockPath: path.join(directory, 'apply.lock'),
|
|
32
|
+
journalPath: path.join(directory, 'apply-operations.v1.json'),
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function resolveSafeApplyStatePath(statePath, workspaceRoot, options = {}) {
|
|
37
|
+
const root = path.resolve(workspaceRoot || process.cwd());
|
|
38
|
+
const target = path.isAbsolute(statePath)
|
|
39
|
+
? path.resolve(statePath)
|
|
40
|
+
: path.resolve(root, statePath);
|
|
41
|
+
assertSafeApplyPath(target, root, options);
|
|
42
|
+
return target;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function resolveSafeApplyPaths(statePath, workspaceRoot, options = {}) {
|
|
46
|
+
const root = path.resolve(workspaceRoot || process.cwd());
|
|
47
|
+
const safeStatePath = resolveSafeApplyStatePath(statePath, root, options);
|
|
48
|
+
const rawPaths = options.paths || resolveApplyPaths(safeStatePath);
|
|
49
|
+
const paths = {};
|
|
50
|
+
for (const key of ['lockPath', 'journalPath']) {
|
|
51
|
+
if (typeof rawPaths[key] !== 'string' || !rawPaths[key]) {
|
|
52
|
+
throw schemaError('SCHEMA_APPLY_PATH_UNSAFE', 'Schema apply path is invalid.');
|
|
53
|
+
}
|
|
54
|
+
paths[key] = path.isAbsolute(rawPaths[key])
|
|
55
|
+
? path.resolve(rawPaths[key])
|
|
56
|
+
: path.resolve(root, rawPaths[key]);
|
|
57
|
+
assertSafeApplyPath(paths[key], root, options);
|
|
58
|
+
}
|
|
59
|
+
return { statePath: safeStatePath, paths };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function assertSafeApplyPath(targetPath, workspaceRoot, options = {}) {
|
|
63
|
+
const fsImpl = options.fsImpl || fs;
|
|
64
|
+
const root = path.resolve(workspaceRoot || process.cwd());
|
|
65
|
+
const target = path.resolve(targetPath);
|
|
66
|
+
if (target === root || !target.startsWith(root + path.sep)) {
|
|
67
|
+
throw schemaError('SCHEMA_APPLY_PATH_UNSAFE', 'Schema apply path must stay inside the workspace.');
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const relativeSegments = path.relative(root, target).split(path.sep);
|
|
71
|
+
let current = root;
|
|
72
|
+
for (const segment of relativeSegments) {
|
|
73
|
+
current = path.join(current, segment);
|
|
74
|
+
let exists;
|
|
75
|
+
try {
|
|
76
|
+
exists = fsImpl.existsSync(current);
|
|
77
|
+
} catch (error) {
|
|
78
|
+
throw schemaError('SCHEMA_APPLY_PATH_UNSAFE', 'Schema apply state path could not be verified.');
|
|
79
|
+
}
|
|
80
|
+
if (!exists) {
|
|
81
|
+
break;
|
|
82
|
+
}
|
|
83
|
+
let stat;
|
|
84
|
+
try {
|
|
85
|
+
stat = fsImpl.lstatSync(current);
|
|
86
|
+
} catch (error) {
|
|
87
|
+
throw schemaError('SCHEMA_APPLY_PATH_UNSAFE', 'Schema apply state path could not be verified.');
|
|
88
|
+
}
|
|
89
|
+
if (stat.isSymbolicLink()) {
|
|
90
|
+
throw schemaError('SCHEMA_APPLY_PATH_UNSAFE', 'Schema apply path must not traverse symbolic links.');
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return true;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function acquireApplyLock(lockPath, options = {}) {
|
|
97
|
+
const owner = createLockOwner(options);
|
|
98
|
+
assertSafeApplyPathIfConfigured(lockPath, options);
|
|
99
|
+
const authority = createApplyLockAuthority(lockPath, options);
|
|
100
|
+
|
|
101
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
102
|
+
try {
|
|
103
|
+
const fileIdentity = createOwnershipFile(lockPath, owner, options);
|
|
104
|
+
assertApplyLockAuthorityCurrent(authority, options.fsImpl || fs);
|
|
105
|
+
return createLockHandle(lockPath, owner, options, fileIdentity, authority);
|
|
106
|
+
} catch (error) {
|
|
107
|
+
if (!error || error.code !== 'EEXIST') {
|
|
108
|
+
if (error && error.code === 'SCHEMA_APPLY_PATH_UNSAFE') {
|
|
109
|
+
throw error;
|
|
110
|
+
}
|
|
111
|
+
throw schemaError('SCHEMA_APPLY_LOCK_FAILED', 'Schema apply lock could not be acquired.');
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
let reclaimed;
|
|
116
|
+
try {
|
|
117
|
+
reclaimed = reclaimStaleLock(lockPath, options);
|
|
118
|
+
} catch (error) {
|
|
119
|
+
if (error && error.code === 'SCHEMA_APPLY_PATH_UNSAFE') {
|
|
120
|
+
throw error;
|
|
121
|
+
}
|
|
122
|
+
throw schemaError('SCHEMA_APPLY_LOCK_FAILED', 'Schema apply stale lock could not be checked.');
|
|
123
|
+
}
|
|
124
|
+
if (!reclaimed) {
|
|
125
|
+
throw schemaError('SCHEMA_APPLY_LOCKED', 'Another schema apply holds the state lock.');
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
throw schemaError('SCHEMA_APPLY_LOCKED', 'Another schema apply holds the state lock.');
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function createLockOwner(options = {}) {
|
|
133
|
+
const pid = options.processId === undefined ? process.pid : options.processId;
|
|
134
|
+
const ownerToken = options.ownerToken || crypto.randomBytes(16).toString('hex');
|
|
135
|
+
if (!Number.isInteger(pid) || pid <= 0 || !OWNER_TOKEN_PATTERN.test(ownerToken)) {
|
|
136
|
+
throw schemaError('SCHEMA_APPLY_LOCK_FAILED', 'Schema apply lock owner is invalid.');
|
|
137
|
+
}
|
|
138
|
+
return Object.freeze({
|
|
139
|
+
kind: APPLY_LOCK_KIND,
|
|
140
|
+
contractVersion: APPLY_LOCK_CONTRACT_VERSION,
|
|
141
|
+
ownerToken,
|
|
142
|
+
pid,
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function createOwnershipFile(lockPath, owner, options = {}) {
|
|
147
|
+
const fsImpl = options.fsImpl || fs;
|
|
148
|
+
const directory = path.dirname(lockPath);
|
|
149
|
+
let fd;
|
|
150
|
+
let fileIdentity;
|
|
151
|
+
let created = false;
|
|
152
|
+
try {
|
|
153
|
+
assertSafeApplyPathIfConfigured(lockPath, options);
|
|
154
|
+
fsImpl.mkdirSync(directory, { recursive: true, mode: 0o700 });
|
|
155
|
+
assertSafeApplyPathIfConfigured(lockPath, options);
|
|
156
|
+
fd = fsImpl.openSync(lockPath, 'wx', 0o600);
|
|
157
|
+
created = true;
|
|
158
|
+
if (typeof fsImpl.fstatSync === 'function') {
|
|
159
|
+
fileIdentity = fsImpl.fstatSync(fd);
|
|
160
|
+
}
|
|
161
|
+
fsImpl.writeFileSync(fd, stableStringify(owner) + '\n', 'utf8');
|
|
162
|
+
if (typeof fsImpl.fsyncSync === 'function') {
|
|
163
|
+
fsImpl.fsyncSync(fd);
|
|
164
|
+
}
|
|
165
|
+
if (typeof fsImpl.fstatSync === 'function') {
|
|
166
|
+
fileIdentity = fsImpl.fstatSync(fd);
|
|
167
|
+
}
|
|
168
|
+
fsImpl.closeSync(fd);
|
|
169
|
+
fd = undefined;
|
|
170
|
+
const finalIdentity = fsImpl.lstatSync(lockPath);
|
|
171
|
+
if (!sameFileIdentityOrDefaultWindowsStatMatch(finalIdentity, fileIdentity, fsImpl)) {
|
|
172
|
+
throw new Error('lock file generation changed');
|
|
173
|
+
}
|
|
174
|
+
return captureOwnershipFileIdentity(finalIdentity, fsImpl);
|
|
175
|
+
} catch (error) {
|
|
176
|
+
if (fd !== undefined) {
|
|
177
|
+
try {
|
|
178
|
+
fsImpl.closeSync(fd);
|
|
179
|
+
} catch (closeError) {
|
|
180
|
+
// Best effort close.
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
if (created) {
|
|
184
|
+
cleanupCreatedOwnershipFile(lockPath, fileIdentity, fsImpl);
|
|
185
|
+
}
|
|
186
|
+
throw error;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function cleanupCreatedOwnershipFile(lockPath, fileIdentity, fsImpl) {
|
|
191
|
+
try {
|
|
192
|
+
const current = fsImpl.lstatSync(lockPath);
|
|
193
|
+
if (fileIdentity && sameFileIdentityOrDefaultWindowsStatMatch(current, fileIdentity, fsImpl)) {
|
|
194
|
+
fsImpl.unlinkSync(lockPath);
|
|
195
|
+
}
|
|
196
|
+
} catch (error) {
|
|
197
|
+
// Best effort cleanup; ownership identity prevents deleting a replacement lock.
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function createLockHandle(lockPath, owner, options = {}, fileIdentity, authority = null) {
|
|
202
|
+
const fsImpl = options.fsImpl || fs;
|
|
203
|
+
const handleOptions = Object.freeze({ ...options, fsImpl });
|
|
204
|
+
const handle = Object.freeze({
|
|
205
|
+
assertOwned() {
|
|
206
|
+
return assertApplyLockHandleOwned(handle);
|
|
207
|
+
},
|
|
208
|
+
release() {
|
|
209
|
+
return releaseApplyLockHandle(handle);
|
|
210
|
+
},
|
|
211
|
+
});
|
|
212
|
+
APPLY_LOCK_HANDLES.set(handle, {
|
|
213
|
+
authority,
|
|
214
|
+
fileIdentity,
|
|
215
|
+
fsImpl,
|
|
216
|
+
lockPath: path.resolve(lockPath),
|
|
217
|
+
options: handleOptions,
|
|
218
|
+
owner,
|
|
219
|
+
released: false,
|
|
220
|
+
});
|
|
221
|
+
return handle;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function assertApplyLockHandleOwned(handle, expectedContext) {
|
|
225
|
+
const state = handle && APPLY_LOCK_HANDLES.get(handle);
|
|
226
|
+
if (!state || state.released) {
|
|
227
|
+
throw schemaError('SCHEMA_APPLY_LOCK_LOST', 'Schema apply lock ownership was lost.');
|
|
228
|
+
}
|
|
229
|
+
try {
|
|
230
|
+
assertSafeApplyPathIfConfigured(state.lockPath, state.options);
|
|
231
|
+
assertApplyLockAuthorityCurrent(state.authority, state.fsImpl);
|
|
232
|
+
assertExpectedApplyLockContext(state, expectedContext);
|
|
233
|
+
assertCurrentLockFileIdentity(state);
|
|
234
|
+
if (!ownersMatch(readLockOwner(state.lockPath, state.fsImpl), state.owner)) {
|
|
235
|
+
throw new Error('lock ownership changed');
|
|
236
|
+
}
|
|
237
|
+
assertCurrentLockFileIdentity(state);
|
|
238
|
+
} catch (error) {
|
|
239
|
+
throw schemaError('SCHEMA_APPLY_LOCK_LOST', 'Schema apply lock ownership was lost.');
|
|
240
|
+
}
|
|
241
|
+
return true;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function releaseApplyLockHandle(handle) {
|
|
245
|
+
const state = handle && APPLY_LOCK_HANDLES.get(handle);
|
|
246
|
+
if (!state || state.released) {
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
state.released = true;
|
|
250
|
+
try {
|
|
251
|
+
assertSafeApplyPathIfConfigured(state.lockPath, state.options);
|
|
252
|
+
assertApplyLockAuthorityCurrent(state.authority, state.fsImpl);
|
|
253
|
+
assertCurrentLockFileIdentity(state);
|
|
254
|
+
if (!removeOwnedLock(state.lockPath, state.owner, state.fsImpl, state.fileIdentity)) {
|
|
255
|
+
throw new Error('lock ownership changed');
|
|
256
|
+
}
|
|
257
|
+
} catch (error) {
|
|
258
|
+
throw schemaError('SCHEMA_APPLY_LOCK_RELEASE_FAILED', 'Schema apply lock could not be released.');
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function reclaimStaleLock(lockPath, options = {}) {
|
|
263
|
+
const fsImpl = options.fsImpl || fs;
|
|
264
|
+
const existing = readLockOwner(lockPath, fsImpl);
|
|
265
|
+
if (!existing || isLockOwnerAlive(existing, options)) {
|
|
266
|
+
return false;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
const reclaimPath = `${lockPath}.reclaim`;
|
|
270
|
+
assertSafeApplyPathIfConfigured(reclaimPath, options);
|
|
271
|
+
const guard = acquireReclaimGuard(reclaimPath, options);
|
|
272
|
+
if (!guard) {
|
|
273
|
+
return false;
|
|
274
|
+
}
|
|
275
|
+
try {
|
|
276
|
+
const current = readLockOwner(lockPath, fsImpl);
|
|
277
|
+
if (!ownersMatch(current, existing) || isLockOwnerAlive(current, options)) {
|
|
278
|
+
return false;
|
|
279
|
+
}
|
|
280
|
+
return removeOwnedLock(lockPath, existing, fsImpl);
|
|
281
|
+
} finally {
|
|
282
|
+
guard.release();
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function acquireReclaimGuard(reclaimPath, options = {}) {
|
|
287
|
+
const owner = createLockOwner(options);
|
|
288
|
+
try {
|
|
289
|
+
const fileIdentity = createOwnershipFile(reclaimPath, owner, options);
|
|
290
|
+
return createLockHandle(reclaimPath, owner, options, fileIdentity);
|
|
291
|
+
} catch (error) {
|
|
292
|
+
if (!error || error.code !== 'EEXIST') {
|
|
293
|
+
throw error;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
const fsImpl = options.fsImpl || fs;
|
|
298
|
+
const existing = readLockOwner(reclaimPath, fsImpl);
|
|
299
|
+
if (!existing || isLockOwnerAlive(existing, options) || !removeOwnedLock(reclaimPath, existing, fsImpl)) {
|
|
300
|
+
return null;
|
|
301
|
+
}
|
|
302
|
+
try {
|
|
303
|
+
const fileIdentity = createOwnershipFile(reclaimPath, owner, options);
|
|
304
|
+
return createLockHandle(reclaimPath, owner, options, fileIdentity);
|
|
305
|
+
} catch (error) {
|
|
306
|
+
if (error && error.code === 'EEXIST') {
|
|
307
|
+
return null;
|
|
308
|
+
}
|
|
309
|
+
throw error;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function readLockOwner(lockPath, fsImpl) {
|
|
314
|
+
let value;
|
|
315
|
+
try {
|
|
316
|
+
value = JSON.parse(fsImpl.readFileSync(lockPath, 'utf8'));
|
|
317
|
+
} catch (error) {
|
|
318
|
+
if (error && error.code === 'ENOENT') {
|
|
319
|
+
return null;
|
|
320
|
+
}
|
|
321
|
+
return null;
|
|
322
|
+
}
|
|
323
|
+
return isValidLockOwner(value) ? value : null;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
function isValidLockOwner(value) {
|
|
327
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
328
|
+
return false;
|
|
329
|
+
}
|
|
330
|
+
const keys = Object.keys(value).sort();
|
|
331
|
+
if (keys.join(',') !== 'contractVersion,kind,ownerToken,pid') {
|
|
332
|
+
return false;
|
|
333
|
+
}
|
|
334
|
+
return (
|
|
335
|
+
value.kind === APPLY_LOCK_KIND &&
|
|
336
|
+
value.contractVersion === APPLY_LOCK_CONTRACT_VERSION &&
|
|
337
|
+
OWNER_TOKEN_PATTERN.test(value.ownerToken) &&
|
|
338
|
+
Number.isInteger(value.pid) &&
|
|
339
|
+
value.pid > 0
|
|
340
|
+
);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
function isLockOwnerAlive(owner, options = {}) {
|
|
344
|
+
const check = options.isProcessAlive || defaultIsProcessAlive;
|
|
345
|
+
try {
|
|
346
|
+
return check(owner.pid) !== false;
|
|
347
|
+
} catch (error) {
|
|
348
|
+
return true;
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
function defaultIsProcessAlive(pid) {
|
|
353
|
+
try {
|
|
354
|
+
process.kill(pid, 0);
|
|
355
|
+
return true;
|
|
356
|
+
} catch (error) {
|
|
357
|
+
return !error || error.code !== 'ESRCH';
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
function removeOwnedLock(lockPath, owner, fsImpl, expectedIdentity) {
|
|
362
|
+
if (expectedIdentity && !sameFileIdentityOrDefaultWindowsStatMatch(fsImpl.lstatSync(lockPath), expectedIdentity, fsImpl)) {
|
|
363
|
+
return false;
|
|
364
|
+
}
|
|
365
|
+
const current = readLockOwner(lockPath, fsImpl);
|
|
366
|
+
if (!ownersMatch(current, owner)) {
|
|
367
|
+
return false;
|
|
368
|
+
}
|
|
369
|
+
if (expectedIdentity && !sameFileIdentityOrDefaultWindowsStatMatch(fsImpl.lstatSync(lockPath), expectedIdentity, fsImpl)) {
|
|
370
|
+
return false;
|
|
371
|
+
}
|
|
372
|
+
fsImpl.unlinkSync(lockPath);
|
|
373
|
+
return true;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
function ownersMatch(left, right) {
|
|
377
|
+
return !!(
|
|
378
|
+
left && right &&
|
|
379
|
+
left.kind === right.kind &&
|
|
380
|
+
left.contractVersion === right.contractVersion &&
|
|
381
|
+
left.ownerToken === right.ownerToken &&
|
|
382
|
+
left.pid === right.pid
|
|
383
|
+
);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
function sameFileIdentity(left, right) {
|
|
387
|
+
return !!(
|
|
388
|
+
hasReliableIdentity(left) &&
|
|
389
|
+
hasReliableIdentity(right) &&
|
|
390
|
+
left.dev === right.dev &&
|
|
391
|
+
left.ino === right.ino
|
|
392
|
+
);
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
function sameFileIdentityOrDefaultWindowsStatMatch(left, right, fsImpl) {
|
|
396
|
+
if (sameFileIdentity(left, right)) {
|
|
397
|
+
return true;
|
|
398
|
+
}
|
|
399
|
+
if (!isDefaultWindowsFs(fsImpl)) {
|
|
400
|
+
return false;
|
|
401
|
+
}
|
|
402
|
+
return sameRegularFileStatFingerprint(left, right);
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
function sameRegularFileStatFingerprint(left, right) {
|
|
406
|
+
return !!(
|
|
407
|
+
left && right &&
|
|
408
|
+
statLooksLikeRegularFile(left) &&
|
|
409
|
+
statLooksLikeRegularFile(right) &&
|
|
410
|
+
left.size === right.size &&
|
|
411
|
+
sameStatTime(left.mtimeMs, right.mtimeMs) &&
|
|
412
|
+
sameStatTime(left.ctimeMs, right.ctimeMs) &&
|
|
413
|
+
sameStatTime(left.birthtimeMs, right.birthtimeMs)
|
|
414
|
+
);
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
function statLooksLikeRegularFile(stat) {
|
|
418
|
+
return typeof stat.isFile !== 'function' || stat.isFile();
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
function sameStatTime(left, right) {
|
|
422
|
+
if (typeof left !== 'number' || typeof right !== 'number') {
|
|
423
|
+
return left === right;
|
|
424
|
+
}
|
|
425
|
+
return Math.abs(left - right) <= 1;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
function isDefaultWindowsFs(fsImpl) {
|
|
429
|
+
if (process.platform !== 'win32') {
|
|
430
|
+
return false;
|
|
431
|
+
}
|
|
432
|
+
return fsImpl === fs || !!(
|
|
433
|
+
fsImpl &&
|
|
434
|
+
fsImpl.lstatSync === fs.lstatSync &&
|
|
435
|
+
fsImpl.fstatSync === fs.fstatSync &&
|
|
436
|
+
fsImpl.realpathSync === fs.realpathSync
|
|
437
|
+
);
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
function createApplyLockAuthority(lockPath, options) {
|
|
441
|
+
if (!options.workspaceRoot) {
|
|
442
|
+
return null;
|
|
443
|
+
}
|
|
444
|
+
const fsImpl = options.fsImpl || fs;
|
|
445
|
+
const workspaceRoot = path.resolve(options.workspaceRoot);
|
|
446
|
+
let before;
|
|
447
|
+
let after;
|
|
448
|
+
let beforeIdentity;
|
|
449
|
+
let afterIdentity;
|
|
450
|
+
try {
|
|
451
|
+
before = fsImpl.lstatSync(workspaceRoot);
|
|
452
|
+
if (
|
|
453
|
+
before.isSymbolicLink() ||
|
|
454
|
+
!before.isDirectory()
|
|
455
|
+
) {
|
|
456
|
+
throw new Error('workspace generation is unsafe');
|
|
457
|
+
}
|
|
458
|
+
beforeIdentity = captureWorkspaceIdentity(before, resolveRealPath(fsImpl, workspaceRoot), fsImpl);
|
|
459
|
+
after = fsImpl.lstatSync(workspaceRoot);
|
|
460
|
+
afterIdentity = captureWorkspaceIdentity(after, resolveRealPath(fsImpl, workspaceRoot), fsImpl);
|
|
461
|
+
if (
|
|
462
|
+
after.isSymbolicLink() ||
|
|
463
|
+
!after.isDirectory() ||
|
|
464
|
+
!sameWorkspaceIdentity(after, beforeIdentity, fsImpl) ||
|
|
465
|
+
afterIdentity.fingerprint !== beforeIdentity.fingerprint
|
|
466
|
+
) {
|
|
467
|
+
throw new Error('workspace generation changed');
|
|
468
|
+
}
|
|
469
|
+
} catch (error) {
|
|
470
|
+
throw schemaError('SCHEMA_APPLY_PATH_UNSAFE', 'Schema apply workspace could not be verified.');
|
|
471
|
+
}
|
|
472
|
+
const normalizedLockPath = path.resolve(lockPath);
|
|
473
|
+
let statePath = null;
|
|
474
|
+
if (options.statePath !== undefined) {
|
|
475
|
+
statePath = resolveSafeApplyStatePath(options.statePath, workspaceRoot, { fsImpl });
|
|
476
|
+
if (path.resolve(resolveApplyPaths(statePath).lockPath) !== normalizedLockPath) {
|
|
477
|
+
throw schemaError('SCHEMA_APPLY_PATH_UNSAFE', 'Schema apply lock path is not canonical.');
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
return Object.freeze({
|
|
481
|
+
lockPath: normalizedLockPath,
|
|
482
|
+
statePath,
|
|
483
|
+
workspaceIdentity: afterIdentity,
|
|
484
|
+
workspaceIdentityHash: createWorkspaceIdentityHash(afterIdentity),
|
|
485
|
+
workspaceRoot,
|
|
486
|
+
});
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
function assertApplyLockAuthorityCurrent(authority, fsImpl) {
|
|
490
|
+
if (!authority) {
|
|
491
|
+
return;
|
|
492
|
+
}
|
|
493
|
+
const current = fsImpl.lstatSync(authority.workspaceRoot);
|
|
494
|
+
const currentIdentity = captureWorkspaceIdentity(current, resolveRealPath(fsImpl, authority.workspaceRoot), fsImpl);
|
|
495
|
+
if (
|
|
496
|
+
current.isSymbolicLink() ||
|
|
497
|
+
!current.isDirectory() ||
|
|
498
|
+
!sameWorkspaceIdentity(current, authority.workspaceIdentity, fsImpl) ||
|
|
499
|
+
currentIdentity.fingerprint !== authority.workspaceIdentity.fingerprint
|
|
500
|
+
) {
|
|
501
|
+
throw new Error('workspace generation changed');
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
function assertExpectedApplyLockContext(state, expected) {
|
|
506
|
+
if (expected === undefined) {
|
|
507
|
+
return;
|
|
508
|
+
}
|
|
509
|
+
const authority = state.authority;
|
|
510
|
+
if (
|
|
511
|
+
!isStrictLockContext(expected) ||
|
|
512
|
+
!authority ||
|
|
513
|
+
authority.lockPath !== path.resolve(expected.lockPath) ||
|
|
514
|
+
authority.statePath !== path.resolve(expected.statePath) ||
|
|
515
|
+
authority.workspaceRoot !== path.resolve(expected.workspaceRoot) ||
|
|
516
|
+
authority.workspaceIdentityHash !== expected.workspaceGenerationHash
|
|
517
|
+
) {
|
|
518
|
+
throw new Error('lock authority context changed');
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
function isStrictLockContext(value) {
|
|
523
|
+
if (
|
|
524
|
+
!value ||
|
|
525
|
+
typeof value !== 'object' ||
|
|
526
|
+
Array.isArray(value) ||
|
|
527
|
+
Object.getPrototypeOf(value) !== Object.prototype ||
|
|
528
|
+
Object.getOwnPropertySymbols(value).length !== 0
|
|
529
|
+
) {
|
|
530
|
+
return false;
|
|
531
|
+
}
|
|
532
|
+
const keys = Object.keys(value).sort();
|
|
533
|
+
if (keys.join(',') !== 'lockPath,statePath,workspaceGenerationHash,workspaceRoot') {
|
|
534
|
+
return false;
|
|
535
|
+
}
|
|
536
|
+
return Object.values(Object.getOwnPropertyDescriptors(value)).every(descriptor => (
|
|
537
|
+
descriptor.enumerable === true &&
|
|
538
|
+
Object.prototype.hasOwnProperty.call(descriptor, 'value') &&
|
|
539
|
+
typeof descriptor.value === 'string'
|
|
540
|
+
));
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
function assertCurrentLockFileIdentity(state) {
|
|
544
|
+
const current = state.fsImpl.lstatSync(state.lockPath);
|
|
545
|
+
if (
|
|
546
|
+
current.isSymbolicLink() ||
|
|
547
|
+
!current.isFile() ||
|
|
548
|
+
!sameFileIdentityOrDefaultWindowsStatMatch(current, state.fileIdentity, state.fsImpl)
|
|
549
|
+
) {
|
|
550
|
+
throw new Error('lock file generation changed');
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
function captureFileIdentity(stat) {
|
|
555
|
+
if (!hasReliableIdentity(stat)) {
|
|
556
|
+
throw new Error('file identity is unavailable');
|
|
557
|
+
}
|
|
558
|
+
return Object.freeze({ dev: stat.dev, ino: stat.ino });
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
function captureOwnershipFileIdentity(stat, fsImpl) {
|
|
562
|
+
if (isDefaultWindowsFs(fsImpl) && statLooksLikeRegularFile(stat)) {
|
|
563
|
+
return Object.freeze({
|
|
564
|
+
birthtimeMs: stat.birthtimeMs,
|
|
565
|
+
ctimeMs: stat.ctimeMs,
|
|
566
|
+
mtimeMs: stat.mtimeMs,
|
|
567
|
+
size: stat.size,
|
|
568
|
+
type: 'defaultWindowsFile',
|
|
569
|
+
});
|
|
570
|
+
}
|
|
571
|
+
if (hasReliableIdentity(stat)) {
|
|
572
|
+
return captureFileIdentity(stat);
|
|
573
|
+
}
|
|
574
|
+
throw new Error('file identity is unavailable');
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
function captureWorkspaceIdentity(stat, realPath, fsImpl) {
|
|
578
|
+
if (isDefaultWindowsFs(fsImpl)) {
|
|
579
|
+
const normalizedRealPath = normalizeRealPath(realPath);
|
|
580
|
+
return Object.freeze({
|
|
581
|
+
fingerprint: hashStable({
|
|
582
|
+
realPath: normalizedRealPath,
|
|
583
|
+
type: 'directory',
|
|
584
|
+
}),
|
|
585
|
+
realPath: normalizedRealPath,
|
|
586
|
+
type: 'defaultWindowsDirectory',
|
|
587
|
+
});
|
|
588
|
+
}
|
|
589
|
+
const fileIdentity = captureFileIdentity(stat);
|
|
590
|
+
return Object.freeze({
|
|
591
|
+
...fileIdentity,
|
|
592
|
+
fingerprint: createFileIdentityHash(fileIdentity),
|
|
593
|
+
type: 'fileIdentity',
|
|
594
|
+
});
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
function sameWorkspaceIdentity(stat, identity, fsImpl) {
|
|
598
|
+
if (sameFileIdentity(stat, identity)) {
|
|
599
|
+
return true;
|
|
600
|
+
}
|
|
601
|
+
if (
|
|
602
|
+
!isDefaultWindowsFs(fsImpl) ||
|
|
603
|
+
!identity ||
|
|
604
|
+
identity.type !== 'defaultWindowsDirectory'
|
|
605
|
+
) {
|
|
606
|
+
return false;
|
|
607
|
+
}
|
|
608
|
+
return (
|
|
609
|
+
stat &&
|
|
610
|
+
typeof stat.isDirectory === 'function' &&
|
|
611
|
+
stat.isDirectory()
|
|
612
|
+
);
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
function createWorkspaceIdentityHash(identity) {
|
|
616
|
+
return identity.fingerprint;
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
function createFileIdentityHash(identity) {
|
|
620
|
+
return hashStable({ dev: String(identity.dev), ino: String(identity.ino) });
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
function resolveRealPath(fsImpl, targetPath) {
|
|
624
|
+
if (typeof fsImpl.realpathSync !== 'function') {
|
|
625
|
+
return path.resolve(targetPath);
|
|
626
|
+
}
|
|
627
|
+
return fsImpl.realpathSync(targetPath);
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
function normalizeRealPath(realPath) {
|
|
631
|
+
const normalized = path.resolve(realPath);
|
|
632
|
+
return process.platform === 'win32' ? normalized.toLowerCase() : normalized;
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
function hasReliableIdentity(stat) {
|
|
636
|
+
return !!(
|
|
637
|
+
stat &&
|
|
638
|
+
isReliableIdentityValue(stat.dev) &&
|
|
639
|
+
isReliableIdentityValue(stat.ino)
|
|
640
|
+
);
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
function isReliableIdentityValue(value) {
|
|
644
|
+
return (Number.isSafeInteger(value) && value >= 0) ||
|
|
645
|
+
(typeof value === 'bigint' && value >= 0n);
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
function assertSafeApplyPathIfConfigured(targetPath, options) {
|
|
649
|
+
if (options.workspaceRoot) {
|
|
650
|
+
assertSafeApplyPath(targetPath, options.workspaceRoot, options);
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
function createApplyJournal(input) {
|
|
655
|
+
const journal = {
|
|
656
|
+
kind: APPLY_JOURNAL_KIND,
|
|
657
|
+
contractVersion: APPLY_STORE_CONTRACT_VERSION,
|
|
658
|
+
environment: createEnvironmentIdentity(input.environment),
|
|
659
|
+
manifestHash: input.manifestHash,
|
|
660
|
+
planId: input.planId,
|
|
661
|
+
operations: {},
|
|
662
|
+
};
|
|
663
|
+
validateApplyJournal(journal, input);
|
|
664
|
+
return journal;
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
function createApplyOperationId(input) {
|
|
668
|
+
return hashStable({
|
|
669
|
+
contractVersion: APPLY_STORE_CONTRACT_VERSION,
|
|
670
|
+
planId: input.planId,
|
|
671
|
+
resourceType: input.resourceType,
|
|
672
|
+
key: input.key,
|
|
673
|
+
operation: input.operation,
|
|
674
|
+
});
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
function readApplyJournal(journalPath, options = {}) {
|
|
678
|
+
const fsImpl = options.fsImpl || fs;
|
|
679
|
+
assertSafeApplyPathIfConfigured(journalPath, options);
|
|
680
|
+
let exists;
|
|
681
|
+
try {
|
|
682
|
+
exists = fsImpl.existsSync(journalPath);
|
|
683
|
+
} catch (error) {
|
|
684
|
+
throw schemaError('SCHEMA_APPLY_JOURNAL_READ_FAILED', 'Schema apply journal could not be read.');
|
|
685
|
+
}
|
|
686
|
+
if (!exists) {
|
|
687
|
+
return null;
|
|
688
|
+
}
|
|
689
|
+
let raw;
|
|
690
|
+
try {
|
|
691
|
+
assertSafeApplyPathIfConfigured(journalPath, options);
|
|
692
|
+
raw = fsImpl.readFileSync(journalPath, 'utf8');
|
|
693
|
+
} catch (error) {
|
|
694
|
+
throw schemaError('SCHEMA_APPLY_JOURNAL_READ_FAILED', 'Schema apply journal could not be read.');
|
|
695
|
+
}
|
|
696
|
+
let journal;
|
|
697
|
+
try {
|
|
698
|
+
journal = JSON.parse(raw);
|
|
699
|
+
} catch (error) {
|
|
700
|
+
throw schemaError('SCHEMA_APPLY_JOURNAL_INVALID', 'Schema apply journal is not valid JSON.');
|
|
701
|
+
}
|
|
702
|
+
validateApplyJournal(journal, options);
|
|
703
|
+
return journal;
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
function writeApplyJournalAtomic(journalPath, journal, options = {}) {
|
|
707
|
+
validateApplyJournal(journal, options);
|
|
708
|
+
return writeJsonAtomic(journalPath, journal, {
|
|
709
|
+
atomicNonce: options.atomicNonce,
|
|
710
|
+
atomicNow: options.atomicNow,
|
|
711
|
+
atomicProcessId: options.atomicProcessId,
|
|
712
|
+
errorCode: 'SCHEMA_APPLY_JOURNAL_WRITE_FAILED',
|
|
713
|
+
fsImpl: options.fsImpl,
|
|
714
|
+
workspaceRoot: options.workspaceRoot,
|
|
715
|
+
});
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
function updateJournalOperation(journal, operation, options = {}) {
|
|
719
|
+
const next = clonePlain(journal);
|
|
720
|
+
const id = `${operation.resourceType}:${operation.key}`;
|
|
721
|
+
next.operations[id] = clonePlain(operation);
|
|
722
|
+
validateApplyJournal(next, options);
|
|
723
|
+
return next;
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
function removeJournalOperation(journal, operation, options = {}) {
|
|
727
|
+
const next = clonePlain(journal);
|
|
728
|
+
const id = `${operation.resourceType}:${operation.key}`;
|
|
729
|
+
const current = next.operations[id];
|
|
730
|
+
if (!current || current.operationId !== operation.operationId) {
|
|
731
|
+
throw schemaError('SCHEMA_APPLY_JOURNAL_INVALID', 'Schema apply journal operation changed before removal.');
|
|
732
|
+
}
|
|
733
|
+
delete next.operations[id];
|
|
734
|
+
validateApplyJournal(next, options);
|
|
735
|
+
return next;
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
function validateApplyJournal(journal, options = {}) {
|
|
739
|
+
const registry = options.registry || createDefaultRegistry();
|
|
740
|
+
assertObject(journal, 'SCHEMA_APPLY_JOURNAL_INVALID');
|
|
741
|
+
assertAllowedKeys(journal, [
|
|
742
|
+
'kind',
|
|
743
|
+
'contractVersion',
|
|
744
|
+
'environment',
|
|
745
|
+
'manifestHash',
|
|
746
|
+
'planId',
|
|
747
|
+
'operations',
|
|
748
|
+
], 'SCHEMA_APPLY_JOURNAL_INVALID');
|
|
749
|
+
if (journal.kind !== APPLY_JOURNAL_KIND || journal.contractVersion !== APPLY_STORE_CONTRACT_VERSION) {
|
|
750
|
+
throw schemaError('SCHEMA_APPLY_JOURNAL_INVALID', 'Schema apply journal contract is unsupported.');
|
|
751
|
+
}
|
|
752
|
+
const environment = createEnvironmentIdentity(journal.environment);
|
|
753
|
+
if (options.environment) {
|
|
754
|
+
const expected = createEnvironmentIdentity(options.environment);
|
|
755
|
+
if (
|
|
756
|
+
environment.environmentKey !== expected.environmentKey ||
|
|
757
|
+
environment.corpIdHash !== expected.corpIdHash
|
|
758
|
+
) {
|
|
759
|
+
throw schemaError('SCHEMA_STATE_ENVIRONMENT_MISMATCH', 'Schema apply journal belongs to a different environment.');
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
assertHash(journal.manifestHash, 'SCHEMA_APPLY_JOURNAL_INVALID');
|
|
763
|
+
assertHash(journal.planId, 'SCHEMA_APPLY_JOURNAL_INVALID');
|
|
764
|
+
assertObject(journal.operations, 'SCHEMA_APPLY_JOURNAL_INVALID');
|
|
765
|
+
|
|
766
|
+
for (const id of Object.keys(journal.operations)) {
|
|
767
|
+
validateJournalOperation(id, journal.operations[id], {
|
|
768
|
+
environment,
|
|
769
|
+
planId: journal.planId,
|
|
770
|
+
registry,
|
|
771
|
+
});
|
|
772
|
+
}
|
|
773
|
+
return journal;
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
function validateJournalOperation(id, operation, options) {
|
|
777
|
+
assertObject(operation, 'SCHEMA_APPLY_JOURNAL_INVALID');
|
|
778
|
+
assertAllowedKeys(operation, [
|
|
779
|
+
'operationId',
|
|
780
|
+
'resourceType',
|
|
781
|
+
'key',
|
|
782
|
+
'operation',
|
|
783
|
+
'adapterVersion',
|
|
784
|
+
'desiredHash',
|
|
785
|
+
'status',
|
|
786
|
+
'checkpoint',
|
|
787
|
+
'createIdentity',
|
|
788
|
+
'stateRevision',
|
|
789
|
+
'stageCheckpoint',
|
|
790
|
+
'suppressedErrors',
|
|
791
|
+
], 'SCHEMA_APPLY_JOURNAL_INVALID');
|
|
792
|
+
if (`${operation.resourceType}:${operation.key}` !== id) {
|
|
793
|
+
throw schemaError('SCHEMA_APPLY_JOURNAL_INVALID', 'Schema apply journal operation key is inconsistent.');
|
|
794
|
+
}
|
|
795
|
+
assertHash(operation.operationId, 'SCHEMA_APPLY_JOURNAL_INVALID');
|
|
796
|
+
if (operation.operationId !== createApplyOperationId({
|
|
797
|
+
planId: options.planId,
|
|
798
|
+
resourceType: operation.resourceType,
|
|
799
|
+
key: operation.key,
|
|
800
|
+
operation: operation.operation,
|
|
801
|
+
})) {
|
|
802
|
+
throw schemaError('SCHEMA_APPLY_JOURNAL_INVALID', 'Schema apply journal operationId is inconsistent.');
|
|
803
|
+
}
|
|
804
|
+
assertHash(operation.desiredHash, 'SCHEMA_APPLY_JOURNAL_INVALID');
|
|
805
|
+
if (!OPERATION_TYPES.has(operation.operation) || !OPERATION_STATUSES.has(operation.status)) {
|
|
806
|
+
throw schemaError('SCHEMA_APPLY_JOURNAL_INVALID', 'Schema apply journal operation status is invalid.');
|
|
807
|
+
}
|
|
808
|
+
const adapter = options.registry.get(operation.resourceType);
|
|
809
|
+
if (operation.adapterVersion !== adapter.adapterVersion) {
|
|
810
|
+
throw schemaError('SCHEMA_STATE_ADAPTER_VERSION_UNSUPPORTED', 'Schema apply journal adapterVersion is unsupported.', {
|
|
811
|
+
details: {
|
|
812
|
+
resourceType: operation.resourceType,
|
|
813
|
+
key: operation.key,
|
|
814
|
+
stateAdapterVersion: operation.adapterVersion,
|
|
815
|
+
adapterVersion: adapter.adapterVersion,
|
|
816
|
+
},
|
|
817
|
+
});
|
|
818
|
+
}
|
|
819
|
+
if (operation.status === 'completed' && !operation.checkpoint) {
|
|
820
|
+
throw schemaError('SCHEMA_APPLY_JOURNAL_INVALID', 'Completed schema apply operation requires a checkpoint.');
|
|
821
|
+
}
|
|
822
|
+
if (operation.stateRevision !== undefined && (
|
|
823
|
+
!Number.isInteger(operation.stateRevision) || operation.stateRevision < 0
|
|
824
|
+
)) {
|
|
825
|
+
throw schemaError('SCHEMA_APPLY_JOURNAL_INVALID', 'Schema apply operation State revision is invalid.');
|
|
826
|
+
}
|
|
827
|
+
if (operation.createIdentity !== undefined) {
|
|
828
|
+
validateCreateIdentityCheckpoint(operation, adapter, options);
|
|
829
|
+
}
|
|
830
|
+
if (operation.stageCheckpoint !== undefined) {
|
|
831
|
+
if (!isStagedAdapter(adapter) || operation.operation === 'stateRepair') {
|
|
832
|
+
throw schemaError('SCHEMA_APPLY_JOURNAL_INVALID', 'Schema apply staged checkpoint is not supported for this operation.');
|
|
833
|
+
}
|
|
834
|
+
adapter.validateStageCheckpoint(operation.stageCheckpoint, {
|
|
835
|
+
desiredManagedHash: operation.desiredHash,
|
|
836
|
+
operation: operation.operation,
|
|
837
|
+
operationStatus: operation.status,
|
|
838
|
+
});
|
|
839
|
+
}
|
|
840
|
+
if (
|
|
841
|
+
operation.status === 'completed' &&
|
|
842
|
+
operation.operation !== 'stateRepair' &&
|
|
843
|
+
isStagedAdapter(adapter) &&
|
|
844
|
+
operation.stageCheckpoint === undefined
|
|
845
|
+
) {
|
|
846
|
+
throw schemaError('SCHEMA_APPLY_JOURNAL_INVALID', 'Completed staged apply operation requires the completed stage.');
|
|
847
|
+
}
|
|
848
|
+
if (operation.checkpoint !== undefined) {
|
|
849
|
+
validateOperationCheckpoint(operation, options);
|
|
850
|
+
}
|
|
851
|
+
if (operation.suppressedErrors !== undefined) {
|
|
852
|
+
validateSuppressedErrors(operation.suppressedErrors);
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
function validateSuppressedErrors(errors) {
|
|
857
|
+
if (!Array.isArray(errors) || errors.length > 10) {
|
|
858
|
+
throw schemaError('SCHEMA_APPLY_JOURNAL_INVALID', 'Schema apply operation suppressed errors are invalid.');
|
|
859
|
+
}
|
|
860
|
+
for (const item of errors) {
|
|
861
|
+
assertObject(item, 'SCHEMA_APPLY_JOURNAL_INVALID');
|
|
862
|
+
assertAllowedKeys(item, [
|
|
863
|
+
'code',
|
|
864
|
+
'phase',
|
|
865
|
+
'resourceType',
|
|
866
|
+
'key',
|
|
867
|
+
'errorCode',
|
|
868
|
+
'adapterCode',
|
|
869
|
+
'errorName',
|
|
870
|
+
], 'SCHEMA_APPLY_JOURNAL_INVALID');
|
|
871
|
+
assertDiagnosticString(item.code, 120);
|
|
872
|
+
assertDiagnosticString(item.phase, 120);
|
|
873
|
+
assertDiagnosticString(item.resourceType, 80);
|
|
874
|
+
assertDiagnosticString(item.key, 200);
|
|
875
|
+
if (item.errorCode !== undefined) {
|
|
876
|
+
assertDiagnosticCodeString(item.errorCode, 120);
|
|
877
|
+
}
|
|
878
|
+
if (item.adapterCode !== undefined) {
|
|
879
|
+
assertDiagnosticCodeString(item.adapterCode, 120);
|
|
880
|
+
}
|
|
881
|
+
if (item.errorName !== undefined) {
|
|
882
|
+
assertDiagnosticString(item.errorName, 80);
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
function assertDiagnosticString(value, maxLength) {
|
|
888
|
+
if (typeof value !== 'string' || value.length === 0 || value.length > maxLength) {
|
|
889
|
+
throw schemaError('SCHEMA_APPLY_JOURNAL_INVALID', 'Schema apply diagnostic string is invalid.');
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
function assertDiagnosticCodeString(value, maxLength) {
|
|
894
|
+
assertDiagnosticString(value, maxLength);
|
|
895
|
+
if (!/^[A-Z0-9_:-]+$/.test(value)) {
|
|
896
|
+
throw schemaError('SCHEMA_APPLY_JOURNAL_INVALID', 'Schema apply diagnostic code is invalid.');
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
function validateCreateIdentityCheckpoint(operation, adapter, options) {
|
|
901
|
+
if (
|
|
902
|
+
operation.operation !== 'create' ||
|
|
903
|
+
operation.stateRevision === undefined ||
|
|
904
|
+
typeof adapter.resumeCreate !== 'function'
|
|
905
|
+
) {
|
|
906
|
+
throw schemaError('SCHEMA_APPLY_JOURNAL_INVALID', 'Schema create identity checkpoint is not supported.');
|
|
907
|
+
}
|
|
908
|
+
const identity = operation.createIdentity;
|
|
909
|
+
assertObject(identity, 'SCHEMA_APPLY_JOURNAL_INVALID');
|
|
910
|
+
assertAllowedKeys(identity, ['bindings'], 'SCHEMA_APPLY_JOURNAL_INVALID');
|
|
911
|
+
assertObject(identity.bindings, 'SCHEMA_APPLY_JOURNAL_INVALID');
|
|
912
|
+
try {
|
|
913
|
+
let state = createEmptyState(options.environment, { registry: options.registry });
|
|
914
|
+
state = upsertResourceState(state, {
|
|
915
|
+
resourceType: operation.resourceType,
|
|
916
|
+
key: operation.key,
|
|
917
|
+
adapterVersion: operation.adapterVersion,
|
|
918
|
+
bindings: identity.bindings,
|
|
919
|
+
}, { registry: options.registry });
|
|
920
|
+
if (!getResourceState(state, operation.resourceType, operation.key)) {
|
|
921
|
+
throw new Error('identity checkpoint missing');
|
|
922
|
+
}
|
|
923
|
+
} catch (error) {
|
|
924
|
+
if (error && error.code === 'SCHEMA_STATE_ADAPTER_VERSION_UNSUPPORTED') {
|
|
925
|
+
throw error;
|
|
926
|
+
}
|
|
927
|
+
throw schemaError('SCHEMA_APPLY_JOURNAL_INVALID', 'Schema create identity checkpoint is invalid.');
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
function validateOperationCheckpoint(operation, options) {
|
|
932
|
+
const checkpoint = operation.checkpoint;
|
|
933
|
+
assertObject(checkpoint, 'SCHEMA_APPLY_JOURNAL_INVALID');
|
|
934
|
+
assertAllowedKeys(checkpoint, [
|
|
935
|
+
'adapterVersion',
|
|
936
|
+
'bindings',
|
|
937
|
+
'lastAppliedHash',
|
|
938
|
+
'observedManagedHash',
|
|
939
|
+
'remoteSchemaHash',
|
|
940
|
+
'lastApplied',
|
|
941
|
+
], 'SCHEMA_APPLY_JOURNAL_INVALID');
|
|
942
|
+
let state;
|
|
943
|
+
try {
|
|
944
|
+
state = createEmptyState(options.environment, { registry: options.registry });
|
|
945
|
+
state = upsertResourceState(state, {
|
|
946
|
+
resourceType: operation.resourceType,
|
|
947
|
+
key: operation.key,
|
|
948
|
+
...checkpoint,
|
|
949
|
+
}, { registry: options.registry });
|
|
950
|
+
} catch (error) {
|
|
951
|
+
if (error && error.code === 'SCHEMA_STATE_ADAPTER_VERSION_UNSUPPORTED') {
|
|
952
|
+
throw error;
|
|
953
|
+
}
|
|
954
|
+
throw schemaError('SCHEMA_APPLY_JOURNAL_INVALID', 'Schema apply checkpoint is invalid.');
|
|
955
|
+
}
|
|
956
|
+
if (!getResourceState(state, operation.resourceType, operation.key)) {
|
|
957
|
+
throw schemaError('SCHEMA_APPLY_JOURNAL_INVALID', 'Schema apply checkpoint could not be validated.');
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
function writeJsonAtomic(targetPath, value, options = {}) {
|
|
962
|
+
const fsImpl = options.fsImpl || fs;
|
|
963
|
+
const directory = path.dirname(targetPath);
|
|
964
|
+
let fd;
|
|
965
|
+
let tmpPath;
|
|
966
|
+
let tempCreated = false;
|
|
967
|
+
try {
|
|
968
|
+
tmpPath = createAtomicTempPath(targetPath, options);
|
|
969
|
+
assertSafeApplyPathIfConfigured(targetPath, options);
|
|
970
|
+
fsImpl.mkdirSync(directory, { recursive: true, mode: 0o700 });
|
|
971
|
+
assertSafeApplyPathIfConfigured(targetPath, options);
|
|
972
|
+
fd = fsImpl.openSync(tmpPath, 'wx', 0o600);
|
|
973
|
+
tempCreated = true;
|
|
974
|
+
fsImpl.writeFileSync(fd, stableStringify(value) + '\n', 'utf8');
|
|
975
|
+
if (typeof fsImpl.fsyncSync === 'function') {
|
|
976
|
+
fsImpl.fsyncSync(fd);
|
|
977
|
+
}
|
|
978
|
+
fsImpl.closeSync(fd);
|
|
979
|
+
fd = undefined;
|
|
980
|
+
fsImpl.renameSync(tmpPath, targetPath);
|
|
981
|
+
fsyncDirectoryBestEffort(fsImpl, directory);
|
|
982
|
+
return value;
|
|
983
|
+
} catch (error) {
|
|
984
|
+
if (fd !== undefined) {
|
|
985
|
+
try {
|
|
986
|
+
fsImpl.closeSync(fd);
|
|
987
|
+
} catch (closeError) {
|
|
988
|
+
// Best effort close.
|
|
989
|
+
}
|
|
990
|
+
}
|
|
991
|
+
try {
|
|
992
|
+
if (tempCreated && tmpPath && fsImpl.existsSync(tmpPath)) {
|
|
993
|
+
fsImpl.unlinkSync(tmpPath);
|
|
994
|
+
}
|
|
995
|
+
} catch (unlinkError) {
|
|
996
|
+
// Best effort cleanup.
|
|
997
|
+
}
|
|
998
|
+
if (error && error.code === 'SCHEMA_APPLY_PATH_UNSAFE') {
|
|
999
|
+
throw error;
|
|
1000
|
+
}
|
|
1001
|
+
throw schemaError(options.errorCode, 'Schema apply file could not be written atomically.');
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
function createAtomicTempPath(targetPath, options = {}) {
|
|
1006
|
+
const directory = path.dirname(targetPath);
|
|
1007
|
+
const base = path.basename(targetPath);
|
|
1008
|
+
const pid = options.atomicProcessId === undefined ? process.pid : options.atomicProcessId;
|
|
1009
|
+
const now = options.atomicNow === undefined ? Date.now() : options.atomicNow;
|
|
1010
|
+
const nonce = options.atomicNonce || crypto.randomBytes(12).toString('hex');
|
|
1011
|
+
if (
|
|
1012
|
+
!Number.isInteger(pid) || pid <= 0 ||
|
|
1013
|
+
!Number.isSafeInteger(now) || now < 0 ||
|
|
1014
|
+
!ATOMIC_NONCE_PATTERN.test(nonce)
|
|
1015
|
+
) {
|
|
1016
|
+
throw new Error('invalid atomic temp identity');
|
|
1017
|
+
}
|
|
1018
|
+
return path.join(directory, `.${base}.${pid}.${now}.${nonce}.tmp`);
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
function fsyncDirectoryBestEffort(fsImpl, directory) {
|
|
1022
|
+
if (typeof fsImpl.openSync !== 'function' || typeof fsImpl.fsyncSync !== 'function') {
|
|
1023
|
+
return;
|
|
1024
|
+
}
|
|
1025
|
+
let fd;
|
|
1026
|
+
try {
|
|
1027
|
+
fd = fsImpl.openSync(directory, 'r');
|
|
1028
|
+
fsImpl.fsyncSync(fd);
|
|
1029
|
+
} catch (error) {
|
|
1030
|
+
// Directory fsync is not portable.
|
|
1031
|
+
} finally {
|
|
1032
|
+
if (fd !== undefined) {
|
|
1033
|
+
try {
|
|
1034
|
+
fsImpl.closeSync(fd);
|
|
1035
|
+
} catch (closeError) {
|
|
1036
|
+
// Best effort close.
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
}
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
function assertObject(value, code) {
|
|
1043
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
1044
|
+
throw schemaError(code, 'Schema apply store value must be an object.');
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
function assertAllowedKeys(value, allowedKeys, code) {
|
|
1049
|
+
const allowed = new Set(allowedKeys);
|
|
1050
|
+
for (const key of Object.keys(value || {})) {
|
|
1051
|
+
if (!allowed.has(key)) {
|
|
1052
|
+
throw schemaError(code, 'Schema apply store contains an unknown property.');
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
function assertHash(value, code) {
|
|
1058
|
+
if (typeof value !== 'string' || !HASH_PATTERN.test(value)) {
|
|
1059
|
+
throw schemaError(code, 'Schema apply store hash is invalid.');
|
|
1060
|
+
}
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
function clonePlain(value) {
|
|
1064
|
+
return value === undefined ? undefined : JSON.parse(JSON.stringify(value));
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
module.exports = Object.freeze({
|
|
1068
|
+
APPLY_JOURNAL_KIND,
|
|
1069
|
+
APPLY_STORE_CONTRACT_VERSION,
|
|
1070
|
+
acquireApplyLock,
|
|
1071
|
+
assertApplyLockHandleOwned,
|
|
1072
|
+
assertSafeApplyPath,
|
|
1073
|
+
createApplyJournal,
|
|
1074
|
+
createApplyOperationId,
|
|
1075
|
+
readApplyJournal,
|
|
1076
|
+
removeJournalOperation,
|
|
1077
|
+
resolveApplyPaths,
|
|
1078
|
+
resolveSafeApplyPaths,
|
|
1079
|
+
resolveSafeApplyStatePath,
|
|
1080
|
+
updateJournalOperation,
|
|
1081
|
+
validateApplyJournal,
|
|
1082
|
+
writeApplyJournalAtomic,
|
|
1083
|
+
});
|