arcane-os 0.1.0-dev.5

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 (248) hide show
  1. package/CHANGELOG.md +154 -0
  2. package/COMMERCIAL-LICENSE.md +13 -0
  3. package/LICENSE +661 -0
  4. package/NOTICE +70 -0
  5. package/README.md +448 -0
  6. package/bin/arcane-test.mjs +741 -0
  7. package/bin/arcane.mjs +5 -0
  8. package/docs/architecture.md +234 -0
  9. package/docs/compatibility.md +36 -0
  10. package/docs/event-manager.md +166 -0
  11. package/docs/platform-targets.md +108 -0
  12. package/docs/publishing.md +203 -0
  13. package/docs/reference/README.md +117 -0
  14. package/docs/reference/arcane-ollama.md +288 -0
  15. package/docs/reference/availability-and-normalization.md +123 -0
  16. package/docs/reference/behavioral-testing.md +86 -0
  17. package/docs/reference/cli.md +569 -0
  18. package/docs/reference/core/README.md +62 -0
  19. package/docs/reference/core/arcane-ai-contracts.md +873 -0
  20. package/docs/reference/core/arcane-api.md +601 -0
  21. package/docs/reference/core/arcane-entities.md +65 -0
  22. package/docs/reference/core/arcane-events.md +134 -0
  23. package/docs/reference/core/ollama-module.md +181 -0
  24. package/docs/reference/core/reference/arcane-api/ai-and-ollama.md +1909 -0
  25. package/docs/reference/core/reference/arcane-api/applications-terminal-capabilities.md +1057 -0
  26. package/docs/reference/core/reference/arcane-api/core-and-events.md +320 -0
  27. package/docs/reference/core/reference/arcane-api/filesystem-storage-preferences-appearance.md +610 -0
  28. package/docs/reference/core/reference/arcane-api/namespaces.md +1157 -0
  29. package/docs/reference/core/reference/arcane-api/platform-installation-users-system.md +1423 -0
  30. package/docs/reference/core/reference/arcane-api/session-provisioning-diagnostics-development.md +315 -0
  31. package/docs/reference/event-manager.md +957 -0
  32. package/docs/reference/inventory/package-api.json +2632 -0
  33. package/docs/reference/inventory/runtime-components.json +934 -0
  34. package/docs/reference/inventory/runtime-entities.json +26 -0
  35. package/docs/reference/inventory/runtime-modules.json +1249 -0
  36. package/docs/reference/protocols.md +242 -0
  37. package/docs/reference/runtime-components.md +1098 -0
  38. package/docs/reference/runtime-entities.md +303 -0
  39. package/docs/reference/runtime-modules.md +2010 -0
  40. package/docs/reference/sdk-api.md +4901 -0
  41. package/docs/roadmap.md +79 -0
  42. package/docs/work-amplification.md +124 -0
  43. package/node_modules/event-pubsub/CHANGELOG.md +55 -0
  44. package/node_modules/event-pubsub/MIGRATION.md +70 -0
  45. package/node_modules/event-pubsub/README.md +363 -0
  46. package/node_modules/event-pubsub/SECURITY.md +37 -0
  47. package/node_modules/event-pubsub/index.js +141 -0
  48. package/node_modules/event-pubsub/licence +21 -0
  49. package/node_modules/event-pubsub/package.json +59 -0
  50. package/node_modules/strong-type/README.md +408 -0
  51. package/node_modules/strong-type/assets/strong-type-header.png +0 -0
  52. package/node_modules/strong-type/index.js +1151 -0
  53. package/node_modules/strong-type/licence +21 -0
  54. package/node_modules/strong-type/node.js +125 -0
  55. package/node_modules/strong-type/package.json +61 -0
  56. package/package.json +95 -0
  57. package/runtime/ARCANE_RUNTIME_RELEASE.json +791 -0
  58. package/runtime/arcane/components/app-bar.html +468 -0
  59. package/runtime/arcane/components/assistant-panel.html +715 -0
  60. package/runtime/arcane/components/calculator.html +8 -0
  61. package/runtime/arcane/components/chart.html +655 -0
  62. package/runtime/arcane/components/chat.html +1225 -0
  63. package/runtime/arcane/components/conversation-view.html +13 -0
  64. package/runtime/arcane/components/dashboard-config.html +341 -0
  65. package/runtime/arcane/components/data-maintenance.html +112 -0
  66. package/runtime/arcane/components/data-view.html +92 -0
  67. package/runtime/arcane/components/directory-picker.html +197 -0
  68. package/runtime/arcane/components/document-inspector.html +252 -0
  69. package/runtime/arcane/components/file-drop.html +264 -0
  70. package/runtime/arcane/components/file-inspector.html +293 -0
  71. package/runtime/arcane/components/file-manager.html +1715 -0
  72. package/runtime/arcane/components/header.html +142 -0
  73. package/runtime/arcane/components/integration-settings.html +14 -0
  74. package/runtime/arcane/components/local-ai-status.html +360 -0
  75. package/runtime/arcane/components/markdown-document.html +1048 -0
  76. package/runtime/arcane/components/markdown-editor.html +360 -0
  77. package/runtime/arcane/components/media-embed.html +8 -0
  78. package/runtime/arcane/components/modal.html +402 -0
  79. package/runtime/arcane/components/output-panel.html +259 -0
  80. package/runtime/arcane/components/preferences-form.html +135 -0
  81. package/runtime/arcane/components/record-timeline.html +105 -0
  82. package/runtime/arcane/components/relationship-board.html +116 -0
  83. package/runtime/arcane/components/screen-capture.html +8 -0
  84. package/runtime/arcane/components/source-code-viewer.html +441 -0
  85. package/runtime/arcane/components/source-explanation.html +124 -0
  86. package/runtime/arcane/components/speech.html +365 -0
  87. package/runtime/arcane/components/summary-strip.html +177 -0
  88. package/runtime/arcane/components/table.html +77 -0
  89. package/runtime/arcane/components/task-progress.html +282 -0
  90. package/runtime/arcane/components/terminal-workspace.html +65 -0
  91. package/runtime/arcane/components/theme-editor.html +41 -0
  92. package/runtime/arcane/components/theme-switcher.html +46 -0
  93. package/runtime/arcane/components/unified-inbox.html +20 -0
  94. package/runtime/arcane/components/voice-transcription.html +476 -0
  95. package/runtime/arcane/components/weather-widget.html +8 -0
  96. package/runtime/arcane/components/web-navigator.html +239 -0
  97. package/runtime/arcane/css/communications.css +1 -0
  98. package/runtime/arcane/css/dashboard-config.css +45 -0
  99. package/runtime/arcane/css/document-site.css +981 -0
  100. package/runtime/arcane/css/layout.css +438 -0
  101. package/runtime/arcane/css/primitives.css +321 -0
  102. package/runtime/arcane/css/theme.css +112 -0
  103. package/runtime/arcane/css/utility-workspace.css +1 -0
  104. package/runtime/arcane/entities/ApiModelRecord.js +20 -0
  105. package/runtime/arcane/entities/Calculation.js +13 -0
  106. package/runtime/arcane/entities/Chat.js +581 -0
  107. package/runtime/arcane/entities/CommunicationMessage.js +29 -0
  108. package/runtime/arcane/entities/CommunicationThread.js +21 -0
  109. package/runtime/arcane/entities/Document.js +10 -0
  110. package/runtime/arcane/entities/File.js +143 -0
  111. package/runtime/arcane/entities/Image.js +135 -0
  112. package/runtime/arcane/entities/IntentEnvelope.js +834 -0
  113. package/runtime/arcane/entities/Preference.js +83 -0
  114. package/runtime/arcane/entities/TWiNPolicyDecision.js +1092 -0
  115. package/runtime/arcane/entities/TerminalSession.js +46 -0
  116. package/runtime/arcane/entities/Theme.js +107 -0
  117. package/runtime/arcane/entities/User.js +1046 -0
  118. package/runtime/arcane/entities/Weather.js +23 -0
  119. package/runtime/arcane/img/arcane-os-everywhere.png +0 -0
  120. package/runtime/arcane/img/arrow-left.png +0 -0
  121. package/runtime/arcane/img/arrow-right.png +0 -0
  122. package/runtime/arcane/img/doc.svg +5 -0
  123. package/runtime/arcane/img/folder.svg +4 -0
  124. package/runtime/arcane/img/image.svg +5 -0
  125. package/runtime/arcane/img/refresh.png +0 -0
  126. package/runtime/arcane/img/send.svg +9 -0
  127. package/runtime/arcane/img/trash.svg +5 -0
  128. package/runtime/arcane/img/upload.svg +5 -0
  129. package/runtime/arcane/modules/AI.js +2048 -0
  130. package/runtime/arcane/modules/AIPreferenceRuntime.js +32 -0
  131. package/runtime/arcane/modules/AIPreferenceTuple.js +92 -0
  132. package/runtime/arcane/modules/AIResponseLength.js +42 -0
  133. package/runtime/arcane/modules/AIResponseURLPolicy.js +626 -0
  134. package/runtime/arcane/modules/AnsiText.js +53 -0
  135. package/runtime/arcane/modules/ApiModelDatabase.js +25 -0
  136. package/runtime/arcane/modules/AppDataScope.js +245 -0
  137. package/runtime/arcane/modules/AppearancePreferences.js +28 -0
  138. package/runtime/arcane/modules/ArcaneCommunicationBridge.js +22 -0
  139. package/runtime/arcane/modules/ArcaneNavigationPolicy.js +135 -0
  140. package/runtime/arcane/modules/ArcaneNetworkPolicy.js +255 -0
  141. package/runtime/arcane/modules/AsyncBoundary.js +161 -0
  142. package/runtime/arcane/modules/BrowserTestSuite.js +326 -0
  143. package/runtime/arcane/modules/CalculatorEngine.js +20 -0
  144. package/runtime/arcane/modules/CaseEvidenceIndexer.js +134 -0
  145. package/runtime/arcane/modules/ChartLibrary.js +35 -0
  146. package/runtime/arcane/modules/ChatRecords.js +13 -0
  147. package/runtime/arcane/modules/CommunicationAppController.js +43 -0
  148. package/runtime/arcane/modules/CommunicationHub.js +16 -0
  149. package/runtime/arcane/modules/CommunicationPreferences.js +13 -0
  150. package/runtime/arcane/modules/CommunicationProviderRegistry.js +16 -0
  151. package/runtime/arcane/modules/ComponentContracts.js +586 -0
  152. package/runtime/arcane/modules/ConfiguredAIChatSession.js +288 -0
  153. package/runtime/arcane/modules/ConversationActionItems.js +488 -0
  154. package/runtime/arcane/modules/ConversationClosingReport.js +274 -0
  155. package/runtime/arcane/modules/ConversationTimebox.js +527 -0
  156. package/runtime/arcane/modules/CoreLocalModelCatalog.js +255 -0
  157. package/runtime/arcane/modules/DBLS.js +171 -0
  158. package/runtime/arcane/modules/DBOPFS.js +1154 -0
  159. package/runtime/arcane/modules/DBOPFSWorker.js +116 -0
  160. package/runtime/arcane/modules/DataMaintenance.js +91 -0
  161. package/runtime/arcane/modules/DevelopmentWorkspace.js +74 -0
  162. package/runtime/arcane/modules/DirectoryPicker.js +109 -0
  163. package/runtime/arcane/modules/DocumentNavigation.js +223 -0
  164. package/runtime/arcane/modules/Errors.js +1025 -0
  165. package/runtime/arcane/modules/GifEncoder.js +29 -0
  166. package/runtime/arcane/modules/HTMLImport.js +114 -0
  167. package/runtime/arcane/modules/InMemoryCommunicationProvider.js +14 -0
  168. package/runtime/arcane/modules/IsolatedModelQuestionRunner.js +275 -0
  169. package/runtime/arcane/modules/LocalAIReadiness.js +870 -0
  170. package/runtime/arcane/modules/LocalAIReadinessController.js +156 -0
  171. package/runtime/arcane/modules/MD.js +111 -0
  172. package/runtime/arcane/modules/Mail.js +352 -0
  173. package/runtime/arcane/modules/MailTransport.mjs +180 -0
  174. package/runtime/arcane/modules/Marked.min.js +71 -0
  175. package/runtime/arcane/modules/MemoryRecords.js +44 -0
  176. package/runtime/arcane/modules/MessageAdvisory.js +38 -0
  177. package/runtime/arcane/modules/ModelDefinition.js +189 -0
  178. package/runtime/arcane/modules/Ollama.js +74 -0
  179. package/runtime/arcane/modules/OllamaModelIdentifier.js +21 -0
  180. package/runtime/arcane/modules/OllamaSettings.js +24 -0
  181. package/runtime/arcane/modules/OpenMeteoWeatherProvider.js +14 -0
  182. package/runtime/arcane/modules/PreferenceStore.js +109 -0
  183. package/runtime/arcane/modules/QRCode.min.js +1 -0
  184. package/runtime/arcane/modules/Questionnaire.js +61 -0
  185. package/runtime/arcane/modules/RecordLinkIndex.js +24 -0
  186. package/runtime/arcane/modules/RecordPassageIndex.js +222 -0
  187. package/runtime/arcane/modules/RecordReviewStore.js +94 -0
  188. package/runtime/arcane/modules/RevocableProjectionLedger.js +1623 -0
  189. package/runtime/arcane/modules/RiskSignalAnalyzer.js +37 -0
  190. package/runtime/arcane/modules/ScamRiskPolicy.js +62 -0
  191. package/runtime/arcane/modules/ScopedOPFSCache.js +183 -0
  192. package/runtime/arcane/modules/ScreenCapture.js +20 -0
  193. package/runtime/arcane/modules/SpeechPlayback.js +581 -0
  194. package/runtime/arcane/modules/StaticDocumentCatalog.js +1248 -0
  195. package/runtime/arcane/modules/SystemAppearance.js +20 -0
  196. package/runtime/arcane/modules/SystemPlatformPresentation.js +51 -0
  197. package/runtime/arcane/modules/SystemToolRegistry.js +28 -0
  198. package/runtime/arcane/modules/TerminalClient.js +52 -0
  199. package/runtime/arcane/modules/TerminalCommandRegistry.js +50 -0
  200. package/runtime/arcane/modules/ThemeBootstrap.js +26 -0
  201. package/runtime/arcane/modules/ThemeManager.js +131 -0
  202. package/runtime/arcane/modules/TimeGuard.js +149 -0
  203. package/runtime/arcane/modules/ToolCallRouter.js +83 -0
  204. package/runtime/arcane/modules/WaitForComponent.js +102 -0
  205. package/runtime/arcane/modules/YouTubeMedia.js +16 -0
  206. package/runtime/arcane/modules/uPlot.LICENSE.txt +21 -0
  207. package/runtime/arcane/modules/uPlot.iife.min.js +2 -0
  208. package/runtime/arcane/modules/uPlot.min.css +1 -0
  209. package/runtime/arcane/security/arcane-network-policy.json +6 -0
  210. package/runtime/strong-type/index.js +352 -0
  211. package/runtime/strong-type/licence +21 -0
  212. package/runtime/strong-type/package.json +45 -0
  213. package/schemas/arcane-app-bundle.schema.json +125 -0
  214. package/schemas/arcane-app.schema.json +329 -0
  215. package/schemas/arcane-lock.schema.json +86 -0
  216. package/schemas/arcane-package.schema.json +224 -0
  217. package/schemas/cli-event.schema.json +122 -0
  218. package/schemas/event-stack.schema.json +152 -0
  219. package/schemas/native-build-plan.schema.json +176 -0
  220. package/schemas/target-adapter.schema.json +117 -0
  221. package/src/app-descriptor.mjs +500 -0
  222. package/src/cli/main.mjs +561 -0
  223. package/src/constants.mjs +31 -0
  224. package/src/dev-server.mjs +718 -0
  225. package/src/doctor.mjs +315 -0
  226. package/src/dom-event-instrumentation.mjs +594 -0
  227. package/src/errors.mjs +75 -0
  228. package/src/event-manager.mjs +1342 -0
  229. package/src/event-queue.mjs +138 -0
  230. package/src/events.mjs +219 -0
  231. package/src/index.mjs +177 -0
  232. package/src/integrated-provider-loader.mjs +432 -0
  233. package/src/native-plan.mjs +698 -0
  234. package/src/native-provider-loader.mjs +1126 -0
  235. package/src/packager/core.mjs +2691 -0
  236. package/src/process.mjs +353 -0
  237. package/src/release-bundle.mjs +2523 -0
  238. package/src/repository.mjs +90 -0
  239. package/src/runtime.mjs +452 -0
  240. package/src/scaffold.mjs +380 -0
  241. package/src/targets/index.mjs +436 -0
  242. package/src/templates/assets/app-icon.png +0 -0
  243. package/src/templates/workspace-template.mjs +388 -0
  244. package/src/testing-loader.mjs +9 -0
  245. package/src/testing.mjs +427 -0
  246. package/src/toolchain.mjs +1335 -0
  247. package/src/update-check.mjs +307 -0
  248. package/src/workspace.mjs +449 -0
@@ -0,0 +1,2523 @@
1
+ import {createHash,randomBytes} from 'node:crypto';
2
+ import {once} from 'node:events';
3
+ import {constants as FS_CONSTANTS} from 'node:fs';
4
+ import {
5
+ link,
6
+ lstat,
7
+ mkdir,
8
+ open,
9
+ realpath,
10
+ rm
11
+ } from 'node:fs/promises';
12
+ import path from 'node:path';
13
+ import {Readable,Transform,Writable} from 'node:stream';
14
+ import {pipeline} from 'node:stream/promises';
15
+ import {createDeflateRaw,createGunzip} from 'node:zlib';
16
+
17
+ import {appDescriptorSha256,projectPackageManifest,validateAppDescriptor} from './app-descriptor.mjs';
18
+ import {SDK_NAME,SDK_VERSION} from './constants.mjs';
19
+ import {ArcaneError,ERROR_CODES,throwIfAborted} from './errors.mjs';
20
+ import {
21
+ authenticateAppReleaseAuthority,
22
+ RELEASE_MANIFEST_NAME,
23
+ PACKAGER_VERSION,
24
+ parseSemver
25
+ } from './packager/core.mjs';
26
+
27
+ export const APP_BUNDLE_MANIFEST_NAME='ARCANE_APP_BUNDLE.json';
28
+ export const APP_BUNDLE_DESCRIPTOR_NAME='arcane-app.json';
29
+ export const APP_BUNDLE_RELEASE_PATH=`payload/${RELEASE_MANIFEST_NAME}`;
30
+ export const APP_BUNDLE_KIND='arcane-app-release-bundle';
31
+ export const APP_BUNDLE_FORMAT='ustar+gzip';
32
+ export const APP_BUNDLE_SCHEMA_VERSION=1;
33
+ export const APP_BUNDLE_EXTENSION='.arcane-app.tar.gz';
34
+ export const APP_BUNDLE_SUPPORTED_SDK_VERSIONS=Object.freeze([SDK_VERSION]);
35
+
36
+ export const APP_BUNDLE_LIMITS=Object.freeze({
37
+ maxCompressedBytes:512*1024*1024,
38
+ maxExpandedBytes:1024*1024*1024,
39
+ maxEntries:16384,
40
+ maxPayloadFiles:16381,
41
+ maxEntryBytes:512*1024*1024,
42
+ maxControlBytes:4*1024*1024,
43
+ maxPathBytes:256,
44
+ maxExpansionRatio:200,
45
+ expansionSlackBytes:16*1024*1024
46
+ });
47
+
48
+ const TAR_BLOCK_BYTES=512;
49
+ const TAR_END_BYTES=TAR_BLOCK_BYTES*2;
50
+ const ARCHIVE_MODE=0o644;
51
+ const GZIP_HEADER=Buffer.from([0x1f,0x8b,0x08,0x00,0x00,0x00,0x00,0x00,0x02,0x03]);
52
+ const READ_ONLY_NO_FOLLOW=FS_CONSTANTS.O_RDONLY|(FS_CONSTANTS.O_NOFOLLOW??0);
53
+ const SHA256_PATTERN=/^[0-9a-f]{64}$/u;
54
+ const APP_ID_PATTERN=/^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/u;
55
+ const WINDOWS_RESERVED_PATTERN=
56
+ /^(?:con|prn|aux|nul|clock\$|conin\$|conout\$|com[1-9¹²³]|lpt[1-9¹²³])(?:[.]|$)/iu;
57
+ const WINDOWS_UNSAFE_FILENAME_CHARACTER_PATTERN=/[<>"|?*]/u;
58
+ const PORTABLE_RESERVED_FILENAME_PATTERN=WINDOWS_RESERVED_PATTERN;
59
+ const CONTROL_PATHS=new Set([
60
+ APP_BUNDLE_MANIFEST_NAME,
61
+ APP_BUNDLE_DESCRIPTOR_NAME,
62
+ APP_BUNDLE_RELEASE_PATH
63
+ ]);
64
+ const textDecoder=new TextDecoder('utf-8',{fatal:true});
65
+
66
+ function fail(message,code=ERROR_CODES.integrityFailed,details){
67
+ throw new ArcaneError(code,message,{details});
68
+ }
69
+
70
+ function appendErrorWarning(error,warning){
71
+ try{
72
+ error.message=`${String(error?.message??error)} ${warning}`;
73
+ }catch{}
74
+ }
75
+
76
+ async function emit(onEvent,event){
77
+ await onEvent?.(Object.freeze(event));
78
+ }
79
+
80
+ function sha256(value){
81
+ return createHash('sha256').update(value).digest('hex');
82
+ }
83
+
84
+ function canonicalJsonBytes(value){
85
+ return Buffer.from(`${JSON.stringify(value,null,2)}\n`,'utf8');
86
+ }
87
+
88
+ function compareText(left,right){
89
+ return Buffer.compare(Buffer.from(String(left),'utf8'),Buffer.from(String(right),'utf8'));
90
+ }
91
+
92
+ function isPlainObject(value){
93
+ if(value===null||typeof value!=='object'||Array.isArray(value))return false;
94
+ const prototype=Object.getPrototypeOf(value);
95
+ return prototype===Object.prototype||prototype===null;
96
+ }
97
+
98
+ function assertExactKeys(value,keys,label){
99
+ if(!isPlainObject(value))fail(`${label} must be a JSON object.`);
100
+ const actual=Object.keys(value);
101
+ if(JSON.stringify(actual)!==JSON.stringify(keys)){
102
+ fail(`${label} must contain exactly these keys in canonical order: ${keys.join(', ')}.`);
103
+ }
104
+ }
105
+
106
+ function assertInteger(value,{minimum=0,maximum=Number.MAX_SAFE_INTEGER,label}){
107
+ if(!Number.isSafeInteger(value)||value<minimum||value>maximum){
108
+ fail(`${label} must be an integer from ${minimum} through ${maximum}.`);
109
+ }
110
+ }
111
+
112
+ function assertSha256(value,label){
113
+ if(typeof value!=='string'||!SHA256_PATTERN.test(value)){
114
+ fail(`${label} must be a lowercase SHA-256 digest.`);
115
+ }
116
+ }
117
+
118
+ function pathKey(value){
119
+ return value.normalize('NFC').toLowerCase();
120
+ }
121
+
122
+ function registerPortablePathTopology(topology,filePath,label){
123
+ const segments=filePath.split('/');
124
+ let prefix='';
125
+ for(let index=0;index<segments.length;index+=1){
126
+ prefix=prefix?`${prefix}/${segments[index]}`:segments[index];
127
+ const key=pathKey(prefix);
128
+ const kind=index===segments.length-1?'file':'directory';
129
+ const existingKind=topology.kinds.get(key);
130
+ if(existingKind&&existingKind!==kind){
131
+ fail(`${label} uses ${prefix} as both a file and a directory.`);
132
+ }
133
+ const spelling=topology.spellings.get(key);
134
+ if(spelling!==undefined&&spelling!==prefix){
135
+ fail(`${label} has a case-colliding path prefix: ${spelling} and ${prefix}.`);
136
+ }
137
+ if(existingKind==='file'&&kind==='file'){
138
+ fail(`${label} has a duplicate path: ${prefix}.`);
139
+ }
140
+ topology.kinds.set(key,kind);
141
+ topology.spellings.set(key,prefix);
142
+ }
143
+ }
144
+
145
+ function validatePortablePathTopology(filePaths,label){
146
+ const topology={kinds:new Map(),spellings:new Map()};
147
+ for(const filePath of filePaths)registerPortablePathTopology(topology,filePath,label);
148
+ }
149
+
150
+ export function validateAppBundlePath(value,label='bundle path'){
151
+ if(typeof value!=='string'||!value||value!==value.normalize('NFC')){
152
+ fail(`${label} must be a nonempty NFC-normalized string.`);
153
+ }
154
+ const bytes=Buffer.byteLength(value,'utf8');
155
+ if(bytes>APP_BUNDLE_LIMITS.maxPathBytes){
156
+ fail(`${label} exceeds the ${APP_BUNDLE_LIMITS.maxPathBytes}-byte USTAR limit.`);
157
+ }
158
+ if(value.startsWith('/')||value.startsWith('\\')||value.includes('\\')
159
+ ||value.includes(':')||/[\u0000-\u001f\u007f]/u.test(value)){
160
+ fail(`${label} is not a canonical relative archive path.`);
161
+ }
162
+ const segments=value.split('/');
163
+ if(segments.some(segment=>!segment||segment==='.'||segment==='..'
164
+ ||WINDOWS_UNSAFE_FILENAME_CHARACTER_PATTERN.test(segment)
165
+ ||/[. ]$/u.test(segment)||WINDOWS_RESERVED_PATTERN.test(segment))){
166
+ fail(`${label} contains an unsafe path segment.`);
167
+ }
168
+ return value;
169
+ }
170
+
171
+ function splitUstarPath(value){
172
+ validateAppBundlePath(value,'USTAR entry path');
173
+ const complete=Buffer.from(value,'utf8');
174
+ if(complete.length<=100){
175
+ return {name:complete,prefix:Buffer.alloc(0)};
176
+ }
177
+ for(let index=value.length-1;index>0;index-=1){
178
+ if(value[index]!=='/')continue;
179
+ const prefix=Buffer.from(value.slice(0,index),'utf8');
180
+ const name=Buffer.from(value.slice(index+1),'utf8');
181
+ if(prefix.length<=155&&name.length>0&&name.length<=100){
182
+ return {name,prefix};
183
+ }
184
+ }
185
+ fail(`USTAR entry path cannot be represented without an extension header: ${value}.`);
186
+ }
187
+
188
+ function writeOctal(header,offset,length,value){
189
+ if(!Number.isSafeInteger(value)||value<0)fail('USTAR numeric fields must be nonnegative integers.');
190
+ const octal=value.toString(8);
191
+ if(octal.length>length-1)fail('USTAR numeric field overflowed its canonical octal field.');
192
+ header.write(`${octal.padStart(length-1,'0')}\0`,offset,length,'ascii');
193
+ }
194
+
195
+ export function createCanonicalUstarHeader(entryPath,size){
196
+ assertInteger(size,{minimum:0,maximum:APP_BUNDLE_LIMITS.maxEntryBytes,label:'USTAR entry size'});
197
+ const {name,prefix}=splitUstarPath(entryPath);
198
+ const header=Buffer.alloc(TAR_BLOCK_BYTES,0);
199
+ name.copy(header,0);
200
+ writeOctal(header,100,8,ARCHIVE_MODE);
201
+ writeOctal(header,108,8,0);
202
+ writeOctal(header,116,8,0);
203
+ writeOctal(header,124,12,size);
204
+ writeOctal(header,136,12,0);
205
+ header.fill(0x20,148,156);
206
+ header[156]=0x30;
207
+ header.write('ustar\0',257,6,'ascii');
208
+ header.write('00',263,2,'ascii');
209
+ prefix.copy(header,345);
210
+ const checksum=header.reduce((total,byte)=>total+byte,0);
211
+ const checksumText=checksum.toString(8).padStart(6,'0');
212
+ if(checksumText.length!==6)fail('USTAR header checksum overflowed its canonical field.');
213
+ header.write(`${checksumText}\0 `,148,8,'ascii');
214
+ return header;
215
+ }
216
+
217
+ function decodeUstarField(field,label){
218
+ const zero=field.indexOf(0);
219
+ const end=zero===-1?field.length:zero;
220
+ if(zero!==-1&&field.subarray(zero).some(byte=>byte!==0)){
221
+ fail(`${label} contains nonzero bytes after its terminator.`);
222
+ }
223
+ try{
224
+ return textDecoder.decode(field.subarray(0,end));
225
+ }catch(error){
226
+ fail(`${label} is not canonical UTF-8.`,ERROR_CODES.integrityFailed,{cause:error.message});
227
+ }
228
+ }
229
+
230
+ function parseCanonicalOctal(field,label){
231
+ const text=field.toString('ascii');
232
+ if(!/^[0-7]+\0$/u.test(text))fail(`${label} is not canonical NUL-terminated octal.`);
233
+ const value=Number.parseInt(text.slice(0,-1),8);
234
+ if(!Number.isSafeInteger(value))fail(`${label} exceeds the safe integer range.`);
235
+ return value;
236
+ }
237
+
238
+ function parseCanonicalUstarHeader(header){
239
+ if(header.length!==TAR_BLOCK_BYTES)fail('USTAR header is incomplete.');
240
+ const name=decodeUstarField(header.subarray(0,100),'USTAR name');
241
+ const prefix=decodeUstarField(header.subarray(345,500),'USTAR prefix');
242
+ const entryPath=prefix?`${prefix}/${name}`:name;
243
+ const size=parseCanonicalOctal(header.subarray(124,136),'USTAR size');
244
+ const canonical=createCanonicalUstarHeader(entryPath,size);
245
+ if(!header.equals(canonical)){
246
+ fail(`USTAR header is not the canonical regular-file header for ${entryPath}.`);
247
+ }
248
+ return {path:entryPath,size};
249
+ }
250
+
251
+ function crcTable(){
252
+ const table=new Uint32Array(256);
253
+ for(let index=0;index<256;index+=1){
254
+ let value=index;
255
+ for(let bit=0;bit<8;bit+=1){
256
+ value=(value&1)?(0xedb88320^(value>>>1)):(value>>>1);
257
+ }
258
+ table[index]=value>>>0;
259
+ }
260
+ return table;
261
+ }
262
+
263
+ const CRC32_TABLE=crcTable();
264
+
265
+ function updateCrc32(current,bytes){
266
+ let value=current;
267
+ for(const byte of bytes){
268
+ value=CRC32_TABLE[(value^byte)&0xff]^(value>>>8);
269
+ }
270
+ return value>>>0;
271
+ }
272
+
273
+ function fileIdentity(info){
274
+ return Object.freeze({
275
+ device:String(info.dev),
276
+ inode:String(info.ino),
277
+ bytes:Number(info.size),
278
+ modifiedNanoseconds:String(info.mtimeNs),
279
+ changedNanoseconds:String(info.ctimeNs),
280
+ links:String(info.nlink)
281
+ });
282
+ }
283
+
284
+ function identityMatches(info,identity){
285
+ return String(info.dev)===identity.device
286
+ &&String(info.ino)===identity.inode
287
+ &&Number(info.size)===identity.bytes
288
+ &&String(info.mtimeNs)===identity.modifiedNanoseconds
289
+ &&String(info.ctimeNs)===identity.changedNanoseconds
290
+ &&String(info.nlink)===identity.links;
291
+ }
292
+
293
+ function identitiesEqual(left,right){
294
+ return left.device===right.device
295
+ &&left.inode===right.inode
296
+ &&left.bytes===right.bytes
297
+ &&left.modifiedNanoseconds===right.modifiedNanoseconds
298
+ &&left.changedNanoseconds===right.changedNanoseconds
299
+ &&left.links===right.links;
300
+ }
301
+
302
+ function fileObjectMatches(info,identity){
303
+ return String(info.dev)===identity.device&&String(info.ino)===identity.inode;
304
+ }
305
+
306
+ function anchoredContentObjectMatches(info,identity){
307
+ return info.isFile()
308
+ &&fileObjectMatches(info,identity)
309
+ &&Number(info.size)===identity.bytes
310
+ &&String(info.nlink)===identity.links;
311
+ }
312
+
313
+ async function anchoredFileSha256(handle,identity,label,{signal}={}){
314
+ throwIfAborted(signal);
315
+ const before=await handle.stat({bigint:true});
316
+ if(!anchoredContentObjectMatches(before,identity)){
317
+ fail(`${label} did not retain its anchored file object before content verification.`);
318
+ }
319
+ const digest=createHash('sha256');
320
+ const buffer=Buffer.allocUnsafe(64*1024);
321
+ let position=0;
322
+ while(position<identity.bytes){
323
+ throwIfAborted(signal);
324
+ const requested=Math.min(buffer.length,identity.bytes-position);
325
+ const result=await handle.read(buffer,0,requested,position);
326
+ if(result.bytesRead===0)fail(`${label} ended before its anchored byte length.`);
327
+ digest.update(buffer.subarray(0,result.bytesRead));
328
+ position+=result.bytesRead;
329
+ }
330
+ throwIfAborted(signal);
331
+ const probe=Buffer.alloc(1);
332
+ const extra=await handle.read(probe,0,1,identity.bytes);
333
+ if(extra.bytesRead!==0)fail(`${label} grew beyond its anchored byte length.`);
334
+ throwIfAborted(signal);
335
+ const after=await handle.stat({bigint:true});
336
+ if(!anchoredContentObjectMatches(after,identity)){
337
+ fail(`${label} changed while its anchored content was verified.`);
338
+ }
339
+ throwIfAborted(signal);
340
+ return Object.freeze({
341
+ sha256:digest.digest('hex'),
342
+ before,
343
+ after
344
+ });
345
+ }
346
+
347
+ async function assertAnchoredContent(handle,identity,expectedSha256,label,{signal}={}){
348
+ const inspected=await anchoredFileSha256(handle,identity,label,{signal});
349
+ if(inspected.sha256!==expectedSha256){
350
+ fail(`${label} did not retain its anchored content identity.`);
351
+ }
352
+ if(!identityMatches(inspected.before,identity)||!identityMatches(inspected.after,identity)){
353
+ fail(`${label} did not retain its anchored metadata identity.`);
354
+ }
355
+ }
356
+
357
+ function isInside(parent,candidate){
358
+ const relative=path.relative(parent,candidate);
359
+ return relative===''||(!relative.startsWith('..')&&!path.isAbsolute(relative));
360
+ }
361
+
362
+ async function openStableFile(filePath,label,{maximum=APP_BUNDLE_LIMITS.maxEntryBytes}={}){
363
+ const before=await lstat(filePath,{bigint:true});
364
+ if(before.isSymbolicLink()||!before.isFile()){
365
+ fail(`${label} must be a regular file, not a link or special entry.`);
366
+ }
367
+ if(before.nlink!==1n){
368
+ fail(`${label} must have exactly one filesystem link.`,ERROR_CODES.policyDenied);
369
+ }
370
+ if(Number(before.size)>maximum)fail(`${label} exceeds its ${maximum}-byte limit.`,ERROR_CODES.policyDenied);
371
+ let handle;
372
+ try{
373
+ handle=await open(filePath,READ_ONLY_NO_FOLLOW);
374
+ }catch(error){
375
+ if(error?.code==='ELOOP')fail(`${label} became a symbolic link.`);
376
+ throw error;
377
+ }
378
+ try{
379
+ const opened=await handle.stat({bigint:true});
380
+ if(!opened.isFile()||opened.nlink!==1n||!identityMatches(opened,fileIdentity(before))){
381
+ fail(`${label} changed while it was opened.`);
382
+ }
383
+ return {handle,identity:fileIdentity(opened)};
384
+ }catch(error){
385
+ await handle.close().catch(()=>{});
386
+ throw error;
387
+ }
388
+ }
389
+
390
+ async function readExactOpenedBytes(opened,label,{signal}={}){
391
+ const bytes=Buffer.alloc(opened.identity.bytes);
392
+ let position=0;
393
+ while(position<bytes.length){
394
+ throwIfAborted(signal);
395
+ const result=await opened.handle.read(bytes,position,bytes.length-position,position);
396
+ if(result.bytesRead===0)fail(`${label} ended before its recorded byte length.`);
397
+ position+=result.bytesRead;
398
+ }
399
+ const probe=Buffer.alloc(1);
400
+ const extra=await opened.handle.read(probe,0,1,opened.identity.bytes);
401
+ if(extra.bytesRead!==0)fail(`${label} grew beyond its recorded byte length.`);
402
+ throwIfAborted(signal);
403
+ const after=await opened.handle.stat({bigint:true});
404
+ if(after.nlink!==1n||!identityMatches(after,opened.identity)){
405
+ fail(`${label} changed while it was read.`);
406
+ }
407
+ return bytes;
408
+ }
409
+
410
+ async function readStableControlFile(filePath,label,{signal}={}){
411
+ throwIfAborted(signal);
412
+ const opened=await openStableFile(filePath,label,{maximum:APP_BUNDLE_LIMITS.maxControlBytes});
413
+ try{
414
+ const bytes=await readExactOpenedBytes(opened,label,{signal});
415
+ const current=await lstat(filePath,{bigint:true});
416
+ if(current.isSymbolicLink()||!current.isFile()||current.nlink!==1n
417
+ ||!identityMatches(current,opened.identity)){
418
+ fail(`${label} path changed while it was read.`);
419
+ }
420
+ return bytes;
421
+ }finally{
422
+ await opened.handle.close();
423
+ }
424
+ }
425
+
426
+ async function* verifiedFileChunks(root,identity,{signal}={}){
427
+ throwIfAborted(signal);
428
+ const filePath=path.join(root,...identity.path.split('/'));
429
+ const opened=await openStableFile(filePath,`release payload ${identity.path}`);
430
+ try{
431
+ const canonicalFile=await realpath(filePath);
432
+ if(!isInside(root,canonicalFile))fail(`Release payload escaped its root: ${identity.path}.`);
433
+ if(opened.identity.bytes!==identity.bytes){
434
+ fail(`Release payload size changed: ${identity.path}.`);
435
+ }
436
+ const digest=createHash('sha256');
437
+ let position=0;
438
+ const buffer=Buffer.allocUnsafe(64*1024);
439
+ while(position<identity.bytes){
440
+ throwIfAborted(signal);
441
+ const requested=Math.min(buffer.length,identity.bytes-position);
442
+ const {bytesRead}=await opened.handle.read(buffer,0,requested,position);
443
+ if(bytesRead===0)fail(`Release payload ended early: ${identity.path}.`);
444
+ const chunk=Buffer.from(buffer.subarray(0,bytesRead));
445
+ digest.update(chunk);
446
+ position+=bytesRead;
447
+ yield chunk;
448
+ }
449
+ const probe=Buffer.alloc(1);
450
+ const extra=await opened.handle.read(probe,0,1,identity.bytes);
451
+ if(extra.bytesRead!==0)fail(`Release payload grew while it was bundled: ${identity.path}.`);
452
+ const after=await opened.handle.stat({bigint:true});
453
+ if(after.nlink!==1n||!identityMatches(after,opened.identity)
454
+ ||digest.digest('hex')!==identity.sha256){
455
+ fail(`Release payload changed while it was bundled: ${identity.path}.`);
456
+ }
457
+ const current=await lstat(filePath,{bigint:true});
458
+ if(current.isSymbolicLink()||!current.isFile()||!identityMatches(current,opened.identity)){
459
+ fail(`Release payload path changed while it was bundled: ${identity.path}.`);
460
+ }
461
+ }finally{
462
+ await opened.handle.close();
463
+ }
464
+ }
465
+
466
+ function validateReleaseInventory(receipt){
467
+ if(!Array.isArray(receipt.files)||receipt.files.length<1
468
+ ||receipt.files.length>APP_BUNDLE_LIMITS.maxPayloadFiles){
469
+ fail(`Verified release inventory must contain 1 through ${APP_BUNDLE_LIMITS.maxPayloadFiles} files.`);
470
+ }
471
+ const keys=new Set();
472
+ let totalBytes=0;
473
+ let previous=null;
474
+ const files=receipt.files.map((file,index)=>{
475
+ assertExactKeys(file,['path','bytes','sha256'],`release file ${index}`);
476
+ const relativePath=validateAppBundlePath(file.path,`release file ${index} path`);
477
+ if(pathKey(relativePath)===pathKey(RELEASE_MANIFEST_NAME)){
478
+ fail(`Release inventory must not contain ${RELEASE_MANIFEST_NAME}.`);
479
+ }
480
+ const key=pathKey(relativePath);
481
+ if(keys.has(key))fail(`Release inventory has a duplicate or case-colliding path: ${relativePath}.`);
482
+ keys.add(key);
483
+ if(previous!==null&&compareText(previous,relativePath)>=0){
484
+ fail('Release inventory must use strict canonical path order.');
485
+ }
486
+ previous=relativePath;
487
+ assertInteger(file.bytes,{
488
+ minimum:0,
489
+ maximum:APP_BUNDLE_LIMITS.maxEntryBytes,
490
+ label:`release file ${relativePath} bytes`
491
+ });
492
+ assertSha256(file.sha256,`release file ${relativePath} sha256`);
493
+ totalBytes+=file.bytes;
494
+ if(!Number.isSafeInteger(totalBytes)||totalBytes>APP_BUNDLE_LIMITS.maxExpandedBytes){
495
+ fail('Release payload exceeds the expanded archive limit.',ERROR_CODES.policyDenied);
496
+ }
497
+ return Object.freeze({path:relativePath,bytes:file.bytes,sha256:file.sha256});
498
+ });
499
+ validatePortablePathTopology(files.map(file=>file.path),'Release inventory');
500
+ if(totalBytes<1){
501
+ fail('Verified release payload must contain at least one byte.',ERROR_CODES.policyDenied);
502
+ }
503
+ if(receipt.fileCount!==files.length||receipt.totalBytes!==totalBytes
504
+ ||receipt.contentSha256!==sha256(JSON.stringify(files))){
505
+ fail('Verified release receipt inventory totals are inconsistent.');
506
+ }
507
+ return Object.freeze(files);
508
+ }
509
+
510
+ function expectedReleaseApp(descriptor){
511
+ return Object.freeze({
512
+ id:descriptor.id,
513
+ displayName:descriptor.displayName,
514
+ version:descriptor.version,
515
+ entry:descriptor.package.entry,
516
+ start:`./apps/${descriptor.id}/${descriptor.package.entry}`,
517
+ security:descriptor.security,
518
+ localAIModelPolicy:descriptor.package.localAIModelPolicy
519
+ ??Object.freeze({verified_only:true,models:Object.freeze([])})
520
+ });
521
+ }
522
+
523
+ function releaseDocumentFromReceipt(receipt,descriptor,files){
524
+ const expectedApp=expectedReleaseApp(descriptor);
525
+ if(receipt.builder!==PACKAGER_VERSION
526
+ ||JSON.stringify(receipt.app)!==JSON.stringify(expectedApp)
527
+ ||!SHA256_PATTERN.test(receipt.policySha256??'')){
528
+ fail('Verified release receipt is not bound to the authored schema-2 descriptor.');
529
+ }
530
+ return Object.freeze({
531
+ schemaVersion:1,
532
+ builder:receipt.builder,
533
+ app:expectedApp,
534
+ policySha256:receipt.policySha256,
535
+ fileCount:receipt.fileCount,
536
+ totalBytes:receipt.totalBytes,
537
+ contentSha256:receipt.contentSha256,
538
+ files
539
+ });
540
+ }
541
+
542
+ function createBundleManifest({descriptor,descriptorBytes,descriptorSha256,release,releaseBytes}){
543
+ const packageSha256=sha256(JSON.stringify(projectPackageManifest(descriptor)));
544
+ return Object.freeze({
545
+ schemaVersion:APP_BUNDLE_SCHEMA_VERSION,
546
+ kind:APP_BUNDLE_KIND,
547
+ format:APP_BUNDLE_FORMAT,
548
+ sdk:Object.freeze({name:SDK_NAME,version:SDK_VERSION}),
549
+ app:Object.freeze({id:descriptor.id,version:descriptor.version}),
550
+ descriptor:Object.freeze({
551
+ path:APP_BUNDLE_DESCRIPTOR_NAME,
552
+ schemaVersion:descriptor.schemaVersion,
553
+ canonicalSha256:descriptorSha256,
554
+ packageSha256,
555
+ fileSha256:sha256(descriptorBytes),
556
+ bytes:descriptorBytes.length
557
+ }),
558
+ release:Object.freeze({
559
+ path:APP_BUNDLE_RELEASE_PATH,
560
+ schemaVersion:release.schemaVersion,
561
+ builder:release.builder,
562
+ policySha256:release.policySha256,
563
+ manifestSha256:sha256(releaseBytes),
564
+ contentSha256:release.contentSha256,
565
+ fileCount:release.fileCount,
566
+ totalBytes:release.totalBytes
567
+ }),
568
+ payload:Object.freeze({
569
+ root:'payload',
570
+ fileCount:release.fileCount,
571
+ totalBytes:release.totalBytes,
572
+ files:release.files
573
+ })
574
+ });
575
+ }
576
+
577
+ class ArchiveFileWriter{
578
+ constructor(handle,state){
579
+ this.handle=handle;
580
+ this.digest=createHash('sha256');
581
+ this.bytes=0;
582
+ this.state=state;
583
+ this.recordState();
584
+ }
585
+
586
+ recordState(){
587
+ this.state.contentBytes=this.bytes;
588
+ this.state.contentSha256=this.digest.copy().digest('hex');
589
+ }
590
+
591
+ async write(chunk){
592
+ const bytes=Buffer.isBuffer(chunk)?chunk:Buffer.from(chunk);
593
+ let offset=0;
594
+ while(offset<bytes.length){
595
+ const result=await this.handle.write(bytes,offset,bytes.length-offset,null);
596
+ if(result.bytesWritten===0)fail('Archive output stopped accepting bytes.');
597
+ this.digest.update(bytes.subarray(offset,offset+result.bytesWritten));
598
+ this.bytes+=result.bytesWritten;
599
+ offset+=result.bytesWritten;
600
+ this.recordState();
601
+ if(this.bytes>APP_BUNDLE_LIMITS.maxCompressedBytes){
602
+ fail('Compressed bundle exceeds its 512 MiB limit.',ERROR_CODES.policyDenied);
603
+ }
604
+ }
605
+ }
606
+
607
+ result(){
608
+ return {bytes:this.bytes,sha256:this.digest.digest('hex')};
609
+ }
610
+ }
611
+
612
+ class Crc32Transform extends Transform{
613
+ constructor(){
614
+ super();
615
+ this.crc=0xffffffff;
616
+ this.bytes=0;
617
+ }
618
+
619
+ _transform(chunk,_encoding,callback){
620
+ this.crc=updateCrc32(this.crc,chunk);
621
+ this.bytes=(this.bytes+chunk.length)>>>0;
622
+ callback(null,chunk);
623
+ }
624
+
625
+ trailer(){
626
+ const trailer=Buffer.alloc(8);
627
+ trailer.writeUInt32LE((this.crc^0xffffffff)>>>0,0);
628
+ trailer.writeUInt32LE(this.bytes>>>0,4);
629
+ return trailer;
630
+ }
631
+ }
632
+
633
+ async function* tarChunks(entries,{releaseRoot,signal,onEvent}={}){
634
+ for(let index=0;index<entries.length;index+=1){
635
+ throwIfAborted(signal);
636
+ const entry=entries[index];
637
+ yield createCanonicalUstarHeader(entry.path,entry.bytes);
638
+ if(entry.buffer){
639
+ yield entry.buffer;
640
+ }else{
641
+ yield* verifiedFileChunks(releaseRoot,entry.identity,{signal});
642
+ }
643
+ const padding=(TAR_BLOCK_BYTES-(entry.bytes%TAR_BLOCK_BYTES))%TAR_BLOCK_BYTES;
644
+ if(padding)yield Buffer.alloc(padding);
645
+ await emit(onEvent,{
646
+ type:'bundle.entry.written',
647
+ path:entry.path,
648
+ bytes:entry.bytes,
649
+ index:index+1,
650
+ entryCount:entries.length
651
+ });
652
+ }
653
+ yield Buffer.alloc(TAR_END_BYTES);
654
+ }
655
+
656
+ async function writeDeterministicGzip(fileHandle,entries,options){
657
+ const writer=new ArchiveFileWriter(fileHandle,options.stagingState);
658
+ await writer.write(GZIP_HEADER);
659
+ const crc=new Crc32Transform();
660
+ const deflate=createDeflateRaw({level:9});
661
+ const sink=new Writable({
662
+ write(chunk,_encoding,callback){
663
+ writer.write(chunk).then(()=>callback(),callback);
664
+ }
665
+ });
666
+ await pipeline(Readable.from(tarChunks(entries,options)),crc,deflate,sink);
667
+ await writer.write(crc.trailer());
668
+ return Object.freeze(writer.result());
669
+ }
670
+
671
+ function validateOutputFilename(outputPath){
672
+ const filename=path.basename(outputPath);
673
+ if(!filename.toLowerCase().endsWith(APP_BUNDLE_EXTENSION)){
674
+ fail(`Bundle output must end in ${APP_BUNDLE_EXTENSION}.`,ERROR_CODES.usage);
675
+ }
676
+ if(filename!==filename.normalize('NFC')||filename.length>255
677
+ ||Buffer.byteLength(filename,'utf8')>255||/[. ]$/u.test(filename)
678
+ ||/[<>:"/\\|?*\u0000-\u001f\u007f]/u.test(filename)
679
+ ||PORTABLE_RESERVED_FILENAME_PATTERN.test(filename)){
680
+ fail('Bundle output filename must be one portable direct filename.',ERROR_CODES.policyDenied);
681
+ }
682
+ }
683
+
684
+ async function acquireArtifactLock(outputPath,{onEvent}={}){
685
+ const lockPath=`${outputPath}.lock`;
686
+ const nonce=randomBytes(16).toString('hex');
687
+ let handle;
688
+ let lockIdentity;
689
+ let documentBytes;
690
+ let created=false;
691
+ try{
692
+ handle=await open(lockPath,'wx+',0o600);
693
+ created=true;
694
+ const acquiredAtUtc=new Date().toISOString();
695
+ const expiresAtUtc=new Date(Date.now()+2*60*60*1000).toISOString();
696
+ documentBytes=Buffer.from(`${JSON.stringify({
697
+ owner:`arcane-os:${process.pid}`,
698
+ nonce,
699
+ artifactPath:outputPath,
700
+ acquiredAtUtc,
701
+ ttlSeconds:7200,
702
+ expiresAtUtc,
703
+ releaseProcedure:'The owning Arcane bundle operation removes this lock after success or failure.',
704
+ staleRecoveryProcedure:'After expiresAtUtc, confirm the recorded process is absent and preserve any adjacent temporary artifact before removing this lock.'
705
+ },null,2)}\n`,'utf8');
706
+ await handle.writeFile(documentBytes);
707
+ await handle.sync();
708
+ const opened=await handle.stat({bigint:true});
709
+ if(!opened.isFile()||opened.nlink!==1n||Number(opened.size)!==documentBytes.length){
710
+ fail('Artifact lock changed while it was acquired.',ERROR_CODES.policyDenied);
711
+ }
712
+ lockIdentity=fileIdentity(opened);
713
+ const current=await lstat(lockPath,{bigint:true});
714
+ if(current.isSymbolicLink()||!current.isFile()||current.nlink!==1n
715
+ ||!identityMatches(current,lockIdentity)){
716
+ fail('Artifact lock path changed while it was acquired.',ERROR_CODES.policyDenied);
717
+ }
718
+ await emit(onEvent,Object.freeze({type:'bundle.lock.written',lockPath,nonce}));
719
+ lockIdentity=await anchoredSinglePathContentIdentity(
720
+ lockPath,
721
+ handle,
722
+ lockIdentity,
723
+ sha256(documentBytes),
724
+ 'Acquired artifact lock'
725
+ );
726
+ await handle.close();
727
+ handle=null;
728
+ }catch(error){
729
+ if(handle&&lockIdentity&&documentBytes){
730
+ try{
731
+ await anchoredSinglePathContentIdentity(
732
+ lockPath,
733
+ handle,
734
+ lockIdentity,
735
+ sha256(documentBytes),
736
+ 'Partial artifact lock cleanup'
737
+ );
738
+ await rm(lockPath);
739
+ }catch(cleanupError){
740
+ if(cleanupError?.code!=='ENOENT'){
741
+ appendErrorWarning(
742
+ error,
743
+ `Partial artifact lock was preserved at ${lockPath}; ${cleanupError.message}`
744
+ );
745
+ }
746
+ }
747
+ }else if(created){
748
+ appendErrorWarning(
749
+ error,
750
+ `Partial artifact lock was preserved at ${lockPath} because its FileHandle identity was unavailable; inspect its nonce and owner before recovery.`
751
+ );
752
+ }
753
+ if(handle){
754
+ try{
755
+ await handle.close();
756
+ }catch(closeError){
757
+ appendErrorWarning(
758
+ error,
759
+ `Lock acquisition handle close warning: ${String(closeError?.message??closeError)}`
760
+ );
761
+ }
762
+ handle=null;
763
+ }
764
+ if(error?.code==='EEXIST'){
765
+ fail(`Another bundle operation owns ${lockPath}. Inspect it before stale-lock recovery.`,ERROR_CODES.policyDenied);
766
+ }
767
+ throw error;
768
+ }
769
+ let released=false;
770
+ return async()=>{
771
+ if(released)return null;
772
+ released=true;
773
+ try{
774
+ const current=await lstat(lockPath,{bigint:true});
775
+ if(current.isSymbolicLink()||!current.isFile()||current.nlink!==1n
776
+ ||!identityMatches(current,lockIdentity)){
777
+ throw new Error('Artifact lock path no longer belongs to this operation.');
778
+ }
779
+ const opened=await openStableFile(lockPath,'artifact operation lock',{
780
+ maximum:APP_BUNDLE_LIMITS.maxControlBytes
781
+ });
782
+ try{
783
+ if(!identitiesEqual(opened.identity,lockIdentity)){
784
+ throw new Error('Artifact lock identity no longer belongs to this operation.');
785
+ }
786
+ const currentBytes=await readExactOpenedBytes(opened,'artifact operation lock');
787
+ if(!currentBytes.equals(documentBytes)){
788
+ throw new Error('Artifact lock nonce or contents changed before release.');
789
+ }
790
+ }finally{
791
+ await opened.handle.close().catch(()=>{});
792
+ }
793
+ const beforeRemove=await lstat(lockPath,{bigint:true});
794
+ if(!identityMatches(beforeRemove,lockIdentity)){
795
+ throw new Error('Artifact lock changed immediately before release.');
796
+ }
797
+ await rm(lockPath);
798
+ return null;
799
+ }catch(error){
800
+ return Object.freeze({
801
+ scope:'artifact-lock',
802
+ path:lockPath,
803
+ message:String(error?.message??error),
804
+ recovery:'Inspect the lock owner and nonce before removing or replacing the lease.'
805
+ });
806
+ }
807
+ };
808
+ }
809
+
810
+ async function inspectOutput(outputPath,{overwrite,signal}){
811
+ try{
812
+ const info=await lstat(outputPath,{bigint:true});
813
+ if(info.isSymbolicLink()||!info.isFile()||info.nlink!==1n){
814
+ fail('Bundle output collision is not a replaceable regular file.',ERROR_CODES.policyDenied);
815
+ }
816
+ if(!overwrite){
817
+ fail('Bundle output already exists; pass overwrite: true or --overwrite to replace it.',ERROR_CODES.policyDenied);
818
+ }
819
+ const opened=await openStableFile(outputPath,'existing bundle output',{
820
+ maximum:APP_BUNDLE_LIMITS.maxCompressedBytes
821
+ });
822
+ try{
823
+ if(!identitiesEqual(opened.identity,fileIdentity(info))){
824
+ fail('Bundle output changed while its overwrite anchor was opened.',ERROR_CODES.policyDenied);
825
+ }
826
+ const inspected=await anchoredFileSha256(
827
+ opened.handle,
828
+ opened.identity,
829
+ 'Existing bundle output',
830
+ {signal}
831
+ );
832
+ if(!identityMatches(inspected.before,opened.identity)
833
+ ||!identityMatches(inspected.after,opened.identity)){
834
+ fail('Bundle output changed while its overwrite content was inspected.',ERROR_CODES.policyDenied);
835
+ }
836
+ return {...opened,contentSha256:inspected.sha256};
837
+ }catch(error){
838
+ try{
839
+ await opened.handle.close();
840
+ }catch(closeError){
841
+ appendErrorWarning(
842
+ error,
843
+ `Prior-output anchor close warning: ${String(closeError?.message??closeError)}`
844
+ );
845
+ }
846
+ throw error;
847
+ }
848
+ }catch(error){
849
+ if(error?.code==='ENOENT')return null;
850
+ throw error;
851
+ }
852
+ }
853
+
854
+ async function assertSingleLinkStaging(temporary,temporaryIdentity){
855
+ const staged=await lstat(temporary,{bigint:true});
856
+ if(staged.isSymbolicLink()||!staged.isFile()||staged.nlink!==1n
857
+ ||!identityMatches(staged,temporaryIdentity)){
858
+ fail('Verified bundle staging changed before atomic promotion.',ERROR_CODES.policyDenied);
859
+ }
860
+ }
861
+
862
+ async function assertLinkedStagingPair(temporary,outputPath,temporaryIdentity,anchorHandle){
863
+ const [staged,output,anchored]=await Promise.all([
864
+ lstat(temporary,{bigint:true}),
865
+ lstat(outputPath,{bigint:true}),
866
+ anchorHandle.stat({bigint:true})
867
+ ]);
868
+ const stagedIdentity=fileIdentity(staged);
869
+ if(staged.isSymbolicLink()||output.isSymbolicLink()
870
+ ||!staged.isFile()||!output.isFile()||!anchored.isFile()
871
+ ||staged.nlink!==2n||output.nlink!==2n
872
+ ||anchored.nlink!==2n
873
+ ||!identityMatches(output,stagedIdentity)
874
+ ||!identityMatches(anchored,stagedIdentity)
875
+ ||!fileObjectMatches(staged,temporaryIdentity)
876
+ ||!fileObjectMatches(output,temporaryIdentity)
877
+ ||Number(staged.size)!==temporaryIdentity.bytes
878
+ ||Number(output.size)!==temporaryIdentity.bytes
879
+ ||String(staged.mtimeNs)!==temporaryIdentity.modifiedNanoseconds
880
+ ||String(output.mtimeNs)!==temporaryIdentity.modifiedNanoseconds){
881
+ fail('Bundle output and staging links did not retain the verified staging object.');
882
+ }
883
+ return stagedIdentity;
884
+ }
885
+
886
+ async function anchoredSinglePathIdentity(filePath,handle,priorIdentity,label){
887
+ const [current,anchored]=await Promise.all([
888
+ lstat(filePath,{bigint:true}),
889
+ handle.stat({bigint:true})
890
+ ]);
891
+ const currentIdentity=fileIdentity(current);
892
+ if(current.isSymbolicLink()||!current.isFile()||current.nlink!==1n
893
+ ||!anchored.isFile()||anchored.nlink!==1n
894
+ ||!identityMatches(anchored,currentIdentity)
895
+ ||(priorIdentity&&(priorIdentity.links==='1'
896
+ ?!identityMatches(current,priorIdentity)
897
+ :(!fileObjectMatches(current,priorIdentity)
898
+ ||Number(current.size)!==priorIdentity.bytes
899
+ ||String(current.mtimeNs)!==priorIdentity.modifiedNanoseconds)))){
900
+ fail(`${label} did not retain its anchored single-link identity.`);
901
+ }
902
+ return currentIdentity;
903
+ }
904
+
905
+ async function anchoredLinkPairIdentity(firstPath,secondPath,handle,priorIdentity,label){
906
+ const [first,second,anchored]=await Promise.all([
907
+ lstat(firstPath,{bigint:true}),
908
+ lstat(secondPath,{bigint:true}),
909
+ handle.stat({bigint:true})
910
+ ]);
911
+ const linkedIdentity=fileIdentity(first);
912
+ if(first.isSymbolicLink()||second.isSymbolicLink()
913
+ ||!first.isFile()||!second.isFile()||!anchored.isFile()
914
+ ||first.nlink!==2n||second.nlink!==2n||anchored.nlink!==2n
915
+ ||!identityMatches(second,linkedIdentity)
916
+ ||!identityMatches(anchored,linkedIdentity)
917
+ ||(priorIdentity.links==='2'
918
+ ?!identityMatches(first,priorIdentity)
919
+ :(!fileObjectMatches(first,priorIdentity)
920
+ ||Number(first.size)!==priorIdentity.bytes
921
+ ||String(first.mtimeNs)!==priorIdentity.modifiedNanoseconds))){
922
+ fail(`${label} did not retain its anchored two-link identity.`);
923
+ }
924
+ return linkedIdentity;
925
+ }
926
+
927
+ async function anchoredSinglePathContentIdentity(
928
+ filePath,
929
+ handle,
930
+ identity,
931
+ expectedSha256,
932
+ label,
933
+ options
934
+ ){
935
+ await assertAnchoredContent(handle,identity,expectedSha256,label,options);
936
+ const current=await anchoredSinglePathIdentity(filePath,handle,identity,label);
937
+ throwIfAborted(options?.signal);
938
+ return current;
939
+ }
940
+
941
+ async function anchoredLinkPairContentIdentity(
942
+ firstPath,
943
+ secondPath,
944
+ handle,
945
+ identity,
946
+ expectedSha256,
947
+ label,
948
+ options
949
+ ){
950
+ await assertAnchoredContent(handle,identity,expectedSha256,label,options);
951
+ const current=await anchoredLinkPairIdentity(firstPath,secondPath,handle,identity,label);
952
+ throwIfAborted(options?.signal);
953
+ return current;
954
+ }
955
+
956
+ function changedStagingCleanupIssue(temporary,message){
957
+ return Object.freeze({
958
+ scope:'artifact-staging',
959
+ path:temporary,
960
+ message,
961
+ recovery:'Inspect the preserved staging path and remove it only after confirming its owner.'
962
+ });
963
+ }
964
+
965
+ function anchoredCleanupResult({issue=null,retryIdentity=null}={}){
966
+ return Object.freeze({issue,retryIdentity});
967
+ }
968
+
969
+ async function cleanupAnchoredTemporary(
970
+ temporary,
971
+ handle,
972
+ expectedIdentity,
973
+ expectedContentSha256,
974
+ expectedContentBytes
975
+ ){
976
+ try{
977
+ const anchored=await handle.stat({bigint:true});
978
+ let current;
979
+ try{
980
+ current=await lstat(temporary,{bigint:true});
981
+ }catch(error){
982
+ if(error?.code==='ENOENT')return anchoredCleanupResult();
983
+ throw error;
984
+ }
985
+ let anchoredIdentity=fileIdentity(anchored);
986
+ if(!anchored.isFile()||anchored.nlink!==1n
987
+ ||current.isSymbolicLink()||!current.isFile()||current.nlink!==1n
988
+ ||!fileObjectMatches(current,anchoredIdentity)
989
+ ||Number(current.size)!==anchoredIdentity.bytes){
990
+ return anchoredCleanupResult({
991
+ issue:changedStagingCleanupIssue(
992
+ temporary,
993
+ `Preserved changed staging path ${temporary}; it is not the exact file object held by the creation handle.`
994
+ )
995
+ });
996
+ }
997
+ if(expectedContentSha256&&Number.isSafeInteger(expectedContentBytes)
998
+ &&expectedContentBytes>=0){
999
+ const contentIdentity=expectedIdentity??Object.freeze({
1000
+ ...anchoredIdentity,
1001
+ bytes:expectedContentBytes
1002
+ });
1003
+ try{
1004
+ anchoredIdentity=await anchoredSinglePathContentIdentity(
1005
+ temporary,
1006
+ handle,
1007
+ contentIdentity,
1008
+ expectedContentSha256,
1009
+ 'Verified bundle staging cleanup'
1010
+ );
1011
+ }catch(error){
1012
+ return anchoredCleanupResult({
1013
+ issue:changedStagingCleanupIssue(
1014
+ temporary,
1015
+ `Preserved changed staging path ${temporary}; ${error.message}`
1016
+ )
1017
+ });
1018
+ }
1019
+ }else{
1020
+ return anchoredCleanupResult({
1021
+ issue:changedStagingCleanupIssue(
1022
+ temporary,
1023
+ `Preserved staging path ${temporary}; no authoritative content identity was available for cleanup.`
1024
+ )
1025
+ });
1026
+ }
1027
+ try{
1028
+ await rm(temporary);
1029
+ }catch(error){
1030
+ if(['EACCES','EBUSY','EPERM'].includes(error?.code)){
1031
+ return anchoredCleanupResult({retryIdentity:anchoredIdentity});
1032
+ }
1033
+ throw error;
1034
+ }
1035
+ try{
1036
+ await lstat(temporary,{bigint:true});
1037
+ return anchoredCleanupResult({
1038
+ issue:changedStagingCleanupIssue(
1039
+ temporary,
1040
+ `A staging path reappeared after anchored cleanup at ${temporary}.`
1041
+ )
1042
+ });
1043
+ }catch(error){
1044
+ if(error?.code==='ENOENT')return anchoredCleanupResult();
1045
+ throw error;
1046
+ }
1047
+ }catch(error){
1048
+ return anchoredCleanupResult({
1049
+ issue:changedStagingCleanupIssue(
1050
+ temporary,
1051
+ `Anchored staging cleanup warning for ${temporary}: ${String(error?.message??error)}`
1052
+ )
1053
+ });
1054
+ }
1055
+ }
1056
+
1057
+ async function cleanupOwnedTemporary(
1058
+ temporary,
1059
+ temporaryIdentity,
1060
+ expectedContentSha256,
1061
+ expectedContentBytes
1062
+ ){
1063
+ let opened;
1064
+ const closeAnchor=async message=>{
1065
+ if(!opened?.handle)return message;
1066
+ const handle=opened.handle;
1067
+ opened=null;
1068
+ try{
1069
+ await handle.close();
1070
+ return message;
1071
+ }catch(error){
1072
+ const warning=`Retry-cleanup anchor close warning: ${String(error?.message??error)}`;
1073
+ return message?`${message} ${warning}`:warning;
1074
+ }
1075
+ };
1076
+ try{
1077
+ const current=await lstat(temporary,{bigint:true});
1078
+ if(!temporaryIdentity||!expectedContentSha256
1079
+ ||!Number.isSafeInteger(expectedContentBytes)||expectedContentBytes<0
1080
+ ||current.isSymbolicLink()||!current.isFile()||current.nlink!==1n
1081
+ ||!fileObjectMatches(current,temporaryIdentity)
1082
+ ||Number(current.size)!==temporaryIdentity.bytes
1083
+ ||temporaryIdentity.bytes!==expectedContentBytes){
1084
+ return changedStagingCleanupIssue(
1085
+ temporary,
1086
+ `Preserved changed staging path ${temporary}; its complete no-follow single-link identity is not owned by this operation.`
1087
+ );
1088
+ }
1089
+ opened=await openStableFile(temporary,'verified bundle staging retry cleanup',{
1090
+ maximum:APP_BUNDLE_LIMITS.maxCompressedBytes
1091
+ });
1092
+ if(!fileObjectMatches(current,opened.identity)
1093
+ ||!fileObjectMatches(current,temporaryIdentity)
1094
+ ||opened.identity.bytes!==temporaryIdentity.bytes){
1095
+ return changedStagingCleanupIssue(
1096
+ temporary,
1097
+ await closeAnchor(
1098
+ `Preserved changed staging path ${temporary}; its retry-cleanup file object is not owned by this operation.`
1099
+ )
1100
+ );
1101
+ }
1102
+ try{
1103
+ await assertAnchoredContent(
1104
+ opened.handle,
1105
+ temporaryIdentity,
1106
+ expectedContentSha256,
1107
+ 'Verified bundle staging retry cleanup'
1108
+ );
1109
+ }catch(error){
1110
+ return changedStagingCleanupIssue(
1111
+ temporary,
1112
+ await closeAnchor(
1113
+ `Preserved changed staging path ${temporary}; ${error.message}`
1114
+ )
1115
+ );
1116
+ }
1117
+ const closeWarning=await closeAnchor(null);
1118
+ if(closeWarning){
1119
+ return changedStagingCleanupIssue(
1120
+ temporary,
1121
+ `Preserved changed staging path ${temporary}; ${closeWarning}`
1122
+ );
1123
+ }
1124
+ const beforeRemove=await lstat(temporary,{bigint:true});
1125
+ if(beforeRemove.isSymbolicLink()||!beforeRemove.isFile()||beforeRemove.nlink!==1n
1126
+ ||!identityMatches(beforeRemove,temporaryIdentity)){
1127
+ return changedStagingCleanupIssue(
1128
+ temporary,
1129
+ `Preserved changed staging path ${temporary}; its retry-cleanup identity changed before removal.`
1130
+ );
1131
+ }
1132
+ await rm(temporary);
1133
+ try{
1134
+ await lstat(temporary,{bigint:true});
1135
+ return changedStagingCleanupIssue(
1136
+ temporary,
1137
+ `A staging path reappeared after exact-identity cleanup at ${temporary}.`
1138
+ );
1139
+ }catch(error){
1140
+ if(error?.code==='ENOENT')return null;
1141
+ throw error;
1142
+ }
1143
+ }catch(error){
1144
+ const closeWarning=await closeAnchor(null);
1145
+ if(error?.code==='ENOENT'&&!closeWarning)return null;
1146
+ return changedStagingCleanupIssue(
1147
+ temporary,
1148
+ `Staging cleanup warning for ${temporary}: ${String(error?.message??error)}`
1149
+ +(closeWarning?` ${closeWarning}`:'')
1150
+ );
1151
+ }
1152
+ }
1153
+
1154
+ async function promoteArtifact(temporary,outputPath,{
1155
+ existingOutput,
1156
+ stagingState,
1157
+ anchorHandle,
1158
+ onEvent,
1159
+ signal
1160
+ }){
1161
+ await assertAnchoredContent(
1162
+ anchorHandle,
1163
+ stagingState.identity,
1164
+ stagingState.contentSha256,
1165
+ 'Verified bundle staging',
1166
+ {signal}
1167
+ );
1168
+ await assertSingleLinkStaging(temporary,stagingState.identity);
1169
+ if(!existingOutput){
1170
+ let promoted=false;
1171
+ try{
1172
+ await emit(onEvent,{
1173
+ type:'bundle.archive.output-vacated',
1174
+ outputPath,
1175
+ replaced:false
1176
+ });
1177
+ await assertAnchoredContent(
1178
+ anchorHandle,
1179
+ stagingState.identity,
1180
+ stagingState.contentSha256,
1181
+ 'Verified bundle staging',
1182
+ {signal}
1183
+ );
1184
+ await assertSingleLinkStaging(temporary,stagingState.identity);
1185
+ throwIfAborted(signal);
1186
+ await link(temporary,outputPath);
1187
+ promoted=true;
1188
+ stagingState.identity=await assertLinkedStagingPair(
1189
+ temporary,
1190
+ outputPath,
1191
+ stagingState.identity,
1192
+ anchorHandle
1193
+ );
1194
+ throwIfAborted(signal);
1195
+ await rm(temporary);
1196
+ stagingState.identity=await anchoredSinglePathIdentity(
1197
+ outputPath,
1198
+ anchorHandle,
1199
+ stagingState.identity,
1200
+ 'Bundle output'
1201
+ );
1202
+ return Object.freeze({
1203
+ outputPath,
1204
+ backupPath:null,
1205
+ backupIdentity:null,
1206
+ backupContentSha256:null,
1207
+ promotedIdentity:stagingState.identity,
1208
+ promotedContentSha256:stagingState.contentSha256,
1209
+ promoted,
1210
+ replaced:false,
1211
+ stagingPath:temporary,
1212
+ stagingState
1213
+ });
1214
+ }catch(error){
1215
+ const failure=error?.code==='EEXIST'
1216
+ ?new ArcaneError(
1217
+ ERROR_CODES.policyDenied,
1218
+ 'Bundle output appeared before create-only promotion; no file was overwritten.',
1219
+ {cause:error}
1220
+ )
1221
+ :error;
1222
+ if(promoted){
1223
+ const rollbackIssues=await rollbackPromotion({
1224
+ outputPath,
1225
+ backupPath:null,
1226
+ backupIdentity:null,
1227
+ backupContentSha256:null,
1228
+ promotedIdentity:stagingState.identity,
1229
+ promotedContentSha256:stagingState.contentSha256,
1230
+ promoted,
1231
+ stagingPath:temporary,
1232
+ stagingState
1233
+ },{promotedHandle:anchorHandle});
1234
+ if(rollbackIssues.length){
1235
+ appendErrorWarning(failure,`Rollback warning: ${rollbackIssues.join('; ')}`);
1236
+ }
1237
+ }
1238
+ throw failure;
1239
+ }
1240
+ }
1241
+ const existingIdentity=existingOutput.identity;
1242
+ const backup=`${outputPath}.backup-${process.pid}-${randomBytes(6).toString('hex')}`;
1243
+ let backupLinked=false;
1244
+ let outputVacated=false;
1245
+ let promoted=false;
1246
+ let backupIdentity=null;
1247
+ const backupContentSha256=existingOutput.contentSha256;
1248
+ try{
1249
+ const beforeBackup=await anchoredSinglePathContentIdentity(
1250
+ outputPath,
1251
+ existingOutput.handle,
1252
+ existingIdentity,
1253
+ backupContentSha256,
1254
+ 'Existing bundle output',
1255
+ {signal}
1256
+ );
1257
+ if(!identitiesEqual(beforeBackup,existingIdentity)){
1258
+ fail('Bundle output changed before atomic promotion; no file was overwritten.',ERROR_CODES.policyDenied);
1259
+ }
1260
+ throwIfAborted(signal);
1261
+ await link(outputPath,backup);
1262
+ backupLinked=true;
1263
+ existingOutput.identity=await anchoredLinkPairIdentity(
1264
+ outputPath,
1265
+ backup,
1266
+ existingOutput.handle,
1267
+ existingOutput.identity,
1268
+ 'Bundle output backup'
1269
+ );
1270
+ await emit(onEvent,{
1271
+ type:'bundle.archive.backup-linked',
1272
+ outputPath,
1273
+ backupPath:backup
1274
+ });
1275
+ existingOutput.identity=await anchoredLinkPairContentIdentity(
1276
+ outputPath,
1277
+ backup,
1278
+ existingOutput.handle,
1279
+ existingOutput.identity,
1280
+ backupContentSha256,
1281
+ 'Bundle output backup',
1282
+ {signal}
1283
+ );
1284
+ throwIfAborted(signal);
1285
+ await rm(outputPath);
1286
+ outputVacated=true;
1287
+ existingOutput.identity=await anchoredSinglePathIdentity(
1288
+ backup,
1289
+ existingOutput.handle,
1290
+ existingOutput.identity,
1291
+ 'Vacated bundle output backup'
1292
+ );
1293
+ backupIdentity=existingOutput.identity;
1294
+ await emit(onEvent,{
1295
+ type:'bundle.archive.output-vacated',
1296
+ outputPath,
1297
+ backupPath:backup,
1298
+ replaced:true
1299
+ });
1300
+ const stagedAfterEvent=await anchoredSinglePathContentIdentity(
1301
+ temporary,
1302
+ anchorHandle,
1303
+ stagingState.identity,
1304
+ stagingState.contentSha256,
1305
+ 'Verified bundle staging',
1306
+ {signal}
1307
+ );
1308
+ if(!identitiesEqual(stagedAfterEvent,stagingState.identity)){
1309
+ fail('Verified bundle staging changed before overwrite promotion.',ERROR_CODES.policyDenied);
1310
+ }
1311
+ const backupAfterEvent=await anchoredSinglePathContentIdentity(
1312
+ backup,
1313
+ existingOutput.handle,
1314
+ backupIdentity,
1315
+ backupContentSha256,
1316
+ 'Preserved bundle backup',
1317
+ {signal}
1318
+ );
1319
+ if(!identitiesEqual(backupAfterEvent,backupIdentity)){
1320
+ fail('Preserved bundle backup changed before overwrite promotion.',ERROR_CODES.policyDenied);
1321
+ }
1322
+ const reboundStaging=await anchoredSinglePathIdentity(
1323
+ temporary,
1324
+ anchorHandle,
1325
+ stagedAfterEvent,
1326
+ 'Verified bundle staging'
1327
+ );
1328
+ const reboundBackup=await anchoredSinglePathIdentity(
1329
+ backup,
1330
+ existingOutput.handle,
1331
+ backupAfterEvent,
1332
+ 'Preserved bundle backup'
1333
+ );
1334
+ if(!identitiesEqual(reboundStaging,stagedAfterEvent)
1335
+ ||!identitiesEqual(reboundBackup,backupAfterEvent)){
1336
+ fail('Bundle staging or preserved backup changed immediately before promotion.',ERROR_CODES.policyDenied);
1337
+ }
1338
+ throwIfAborted(signal);
1339
+ await link(temporary,outputPath);
1340
+ promoted=true;
1341
+ stagingState.identity=await assertLinkedStagingPair(
1342
+ temporary,
1343
+ outputPath,
1344
+ stagingState.identity,
1345
+ anchorHandle
1346
+ );
1347
+ throwIfAborted(signal);
1348
+ await rm(temporary);
1349
+ stagingState.identity=await anchoredSinglePathIdentity(
1350
+ outputPath,
1351
+ anchorHandle,
1352
+ stagingState.identity,
1353
+ 'Bundle output'
1354
+ );
1355
+ return Object.freeze({
1356
+ outputPath,
1357
+ backupPath:backup,
1358
+ backupIdentity,
1359
+ backupContentSha256,
1360
+ promotedIdentity:stagingState.identity,
1361
+ promotedContentSha256:stagingState.contentSha256,
1362
+ promoted,
1363
+ replaced:true,
1364
+ stagingPath:temporary,
1365
+ stagingState
1366
+ });
1367
+ }catch(error){
1368
+ const failure=error?.code==='EEXIST'
1369
+ ?new ArcaneError(
1370
+ ERROR_CODES.policyDenied,
1371
+ 'A path collision blocked create-only bundle promotion; no path was overwritten.',
1372
+ {cause:error}
1373
+ )
1374
+ :error;
1375
+ if(outputVacated||promoted){
1376
+ const rollbackIssues=await rollbackPromotion({
1377
+ outputPath,
1378
+ backupPath:backupLinked?backup:null,
1379
+ backupIdentity,
1380
+ backupContentSha256,
1381
+ promotedIdentity:stagingState.identity,
1382
+ promotedContentSha256:stagingState.contentSha256,
1383
+ promoted,
1384
+ stagingPath:temporary,
1385
+ stagingState
1386
+ },{
1387
+ promotedHandle:anchorHandle,
1388
+ backupHandle:existingOutput.handle
1389
+ });
1390
+ if(rollbackIssues.length){
1391
+ appendErrorWarning(failure,`Rollback warning: ${rollbackIssues.join('; ')}`);
1392
+ }
1393
+ }else if(backupLinked){
1394
+ const cleanupIssues=await removeUncommittedBackupLink({
1395
+ outputPath,
1396
+ backupPath:backup,
1397
+ existingOutput
1398
+ });
1399
+ if(cleanupIssues.length){
1400
+ appendErrorWarning(failure,`Backup cleanup warning: ${cleanupIssues.join('; ')}`);
1401
+ }
1402
+ }
1403
+ throw failure;
1404
+ }
1405
+ }
1406
+
1407
+ async function removeUncommittedBackupLink({outputPath,backupPath,existingOutput}){
1408
+ const issues=[];
1409
+ try{
1410
+ existingOutput.identity=await anchoredLinkPairContentIdentity(
1411
+ outputPath,
1412
+ backupPath,
1413
+ existingOutput.handle,
1414
+ existingOutput.identity,
1415
+ existingOutput.contentSha256,
1416
+ 'Uncommitted bundle backup'
1417
+ );
1418
+ await rm(backupPath);
1419
+ existingOutput.identity=await anchoredSinglePathContentIdentity(
1420
+ outputPath,
1421
+ existingOutput.handle,
1422
+ existingOutput.identity,
1423
+ existingOutput.contentSha256,
1424
+ 'Existing output after backup cleanup'
1425
+ );
1426
+ }catch(error){
1427
+ issues.push(`preserve uncommitted backup ${backupPath}: ${error.message}`);
1428
+ }
1429
+ return issues;
1430
+ }
1431
+
1432
+ async function stableFileDigest(filePath,label,{signal}={}){
1433
+ const opened=await openStableFile(filePath,label,{
1434
+ maximum:APP_BUNDLE_LIMITS.maxCompressedBytes
1435
+ });
1436
+ let operationError;
1437
+ try{
1438
+ const digest=createHash('sha256');
1439
+ const buffer=Buffer.allocUnsafe(64*1024);
1440
+ let position=0;
1441
+ while(position<opened.identity.bytes){
1442
+ throwIfAborted(signal);
1443
+ const requested=Math.min(buffer.length,opened.identity.bytes-position);
1444
+ const result=await opened.handle.read(buffer,0,requested,position);
1445
+ if(result.bytesRead===0)fail(`${label} ended before its recorded byte length.`);
1446
+ digest.update(buffer.subarray(0,result.bytesRead));
1447
+ position+=result.bytesRead;
1448
+ }
1449
+ const probe=Buffer.alloc(1);
1450
+ const extra=await opened.handle.read(probe,0,1,opened.identity.bytes);
1451
+ if(extra.bytesRead!==0)fail(`${label} grew beyond its recorded byte length.`);
1452
+ const canonicalPath=await realpath(filePath);
1453
+ const after=await opened.handle.stat({bigint:true});
1454
+ const current=await lstat(filePath,{bigint:true});
1455
+ const canonical=await lstat(canonicalPath,{bigint:true});
1456
+ if(after.nlink!==1n||current.nlink!==1n||canonical.nlink!==1n
1457
+ ||current.isSymbolicLink()||canonical.isSymbolicLink()
1458
+ ||!current.isFile()||!identityMatches(after,opened.identity)
1459
+ ||!canonical.isFile()||!identityMatches(current,opened.identity)
1460
+ ||!identityMatches(canonical,opened.identity)){
1461
+ fail(`${label} changed while its promoted identity was verified.`);
1462
+ }
1463
+ throwIfAborted(signal);
1464
+ return Object.freeze({
1465
+ path:canonicalPath,
1466
+ bytes:opened.identity.bytes,
1467
+ sha256:digest.digest('hex'),
1468
+ identity:opened.identity
1469
+ });
1470
+ }catch(error){
1471
+ operationError=error;
1472
+ throw error;
1473
+ }finally{
1474
+ try{
1475
+ await opened.handle.close();
1476
+ }catch(closeError){
1477
+ if(operationError){
1478
+ appendErrorWarning(
1479
+ operationError,
1480
+ `${label} handle close warning: ${String(closeError?.message??closeError)}`
1481
+ );
1482
+ }else{
1483
+ throw closeError;
1484
+ }
1485
+ }
1486
+ }
1487
+ }
1488
+
1489
+ async function rollbackPromotion(transaction,{promotedHandle,backupHandle}={}){
1490
+ const issues=[];
1491
+ let outputAbsent=false;
1492
+ let recoverableBackupIdentity=null;
1493
+ if(transaction.backupPath){
1494
+ if(!transaction.backupIdentity||!transaction.backupContentSha256||!backupHandle){
1495
+ issues.push(`preserve ${transaction.backupPath}; backup identity is unavailable before restore`);
1496
+ issues.push(`preserve promoted output path ${transaction.outputPath}; prior backup is not recoverable`);
1497
+ return issues;
1498
+ }
1499
+ try{
1500
+ recoverableBackupIdentity=await anchoredSinglePathIdentity(
1501
+ transaction.backupPath,
1502
+ backupHandle,
1503
+ transaction.backupIdentity,
1504
+ 'Preserved bundle backup'
1505
+ );
1506
+ if(!identitiesEqual(recoverableBackupIdentity,transaction.backupIdentity)){
1507
+ throw new Error('Preserved bundle backup identity changed before restore.');
1508
+ }
1509
+ await assertAnchoredContent(
1510
+ backupHandle,
1511
+ recoverableBackupIdentity,
1512
+ transaction.backupContentSha256,
1513
+ 'Preserved bundle backup'
1514
+ );
1515
+ }catch(error){
1516
+ if(error?.code==='ENOENT'){
1517
+ issues.push(`preserved backup disappeared before restore: ${transaction.backupPath}`);
1518
+ }else{
1519
+ issues.push(`preserve ${transaction.backupPath}; backup identity changed before restore: ${error.message}`);
1520
+ }
1521
+ issues.push(`preserve promoted output path ${transaction.outputPath}; prior backup is not recoverable`);
1522
+ return issues;
1523
+ }
1524
+ }
1525
+ try{
1526
+ const output=await lstat(transaction.outputPath,{bigint:true});
1527
+ let anchored;
1528
+ if(promotedHandle){
1529
+ try{
1530
+ anchored=await promotedHandle.stat({bigint:true});
1531
+ }catch(error){
1532
+ issues.push(`preserve changed output path ${transaction.outputPath}; promoted handle identity is unavailable: ${error.message}`);
1533
+ }
1534
+ }
1535
+ let ownedContent=false;
1536
+ if(transaction.promoted&&transaction.promotedIdentity
1537
+ &&transaction.promotedContentSha256&&promotedHandle
1538
+ &&!output.isSymbolicLink()&&output.isFile()
1539
+ &&fileObjectMatches(output,transaction.promotedIdentity)
1540
+ &&Number(output.size)===transaction.promotedIdentity.bytes
1541
+ &&anchored?.isFile()
1542
+ &&fileObjectMatches(anchored,transaction.promotedIdentity)
1543
+ &&Number(anchored.size)===transaction.promotedIdentity.bytes){
1544
+ try{
1545
+ const reboundIdentity=await anchoredSinglePathContentIdentity(
1546
+ transaction.outputPath,
1547
+ promotedHandle,
1548
+ transaction.promotedIdentity,
1549
+ transaction.promotedContentSha256,
1550
+ 'Promoted bundle output rollback'
1551
+ );
1552
+ ownedContent=identitiesEqual(reboundIdentity,transaction.promotedIdentity);
1553
+ }catch(error){
1554
+ issues.push(`preserve changed output path ${transaction.outputPath}; ${error.message}`);
1555
+ }
1556
+ }
1557
+ if(!ownedContent){
1558
+ if(!issues.some(issue=>issue.startsWith(`preserve changed output path ${transaction.outputPath};`))){
1559
+ issues.push(`preserve changed output path ${transaction.outputPath}; identity is not owned by this operation`);
1560
+ }
1561
+ }else{
1562
+ await rm(transaction.outputPath);
1563
+ outputAbsent=true;
1564
+ if(transaction.stagingPath&&transaction.stagingState&&promotedHandle){
1565
+ try{
1566
+ transaction.stagingState.identity=await anchoredSinglePathIdentity(
1567
+ transaction.stagingPath,
1568
+ promotedHandle,
1569
+ transaction.promotedIdentity,
1570
+ 'Rolled-back bundle staging'
1571
+ );
1572
+ }catch(error){
1573
+ if(error?.code!=='ENOENT'){
1574
+ issues.push(`refresh rolled-back staging identity: ${error.message}`);
1575
+ }
1576
+ }
1577
+ }
1578
+ }
1579
+ }catch(error){
1580
+ if(error?.code==='ENOENT')outputAbsent=true;
1581
+ else issues.push(`inspect or remove failed output: ${error.message}`);
1582
+ }
1583
+ if(transaction.backupPath){
1584
+ try{
1585
+ const backupIdentity=await anchoredSinglePathContentIdentity(
1586
+ transaction.backupPath,
1587
+ backupHandle,
1588
+ recoverableBackupIdentity,
1589
+ transaction.backupContentSha256,
1590
+ 'Preserved bundle backup'
1591
+ );
1592
+ if(!identitiesEqual(backupIdentity,recoverableBackupIdentity)){
1593
+ issues.push(`preserve ${transaction.backupPath}; backup identity changed before restore`);
1594
+ return issues;
1595
+ }
1596
+ if(!outputAbsent){
1597
+ issues.push(`preserve ${transaction.backupPath}; output path is occupied by an unowned identity`);
1598
+ return issues;
1599
+ }
1600
+ await link(transaction.backupPath,transaction.outputPath);
1601
+ const linkedIdentity=await anchoredLinkPairContentIdentity(
1602
+ transaction.outputPath,
1603
+ transaction.backupPath,
1604
+ backupHandle,
1605
+ transaction.backupIdentity,
1606
+ transaction.backupContentSha256,
1607
+ 'Create-only bundle restore'
1608
+ );
1609
+ await rm(transaction.backupPath);
1610
+ await anchoredSinglePathContentIdentity(
1611
+ transaction.outputPath,
1612
+ backupHandle,
1613
+ linkedIdentity,
1614
+ transaction.backupContentSha256,
1615
+ 'Restored bundle output'
1616
+ );
1617
+ }catch(error){
1618
+ if(error?.code==='EEXIST'){
1619
+ issues.push(`preserve ${transaction.backupPath}; create-only restore found an occupied output path`);
1620
+ }else if(error?.code!=='ENOENT'){
1621
+ issues.push(`restore ${transaction.backupPath}: ${error.message}`);
1622
+ }
1623
+ else issues.push(`preserved backup disappeared before restore: ${transaction.backupPath}`);
1624
+ }
1625
+ }
1626
+ return issues;
1627
+ }
1628
+
1629
+ async function finalizePromotion(transaction,{backupHandle}={}){
1630
+ if(!transaction.backupPath)return null;
1631
+ try{
1632
+ if(!transaction.backupIdentity||!transaction.backupContentSha256||!backupHandle){
1633
+ throw new Error('Preserved artifact backup identity changed before cleanup.');
1634
+ }
1635
+ const backupIdentity=await anchoredSinglePathContentIdentity(
1636
+ transaction.backupPath,
1637
+ backupHandle,
1638
+ transaction.backupIdentity,
1639
+ transaction.backupContentSha256,
1640
+ 'Preserved artifact backup'
1641
+ );
1642
+ if(!identitiesEqual(backupIdentity,transaction.backupIdentity)){
1643
+ throw new Error('Preserved artifact backup identity changed before cleanup.');
1644
+ }
1645
+ await rm(transaction.backupPath);
1646
+ return null;
1647
+ }catch(error){
1648
+ return Object.freeze({
1649
+ scope:'artifact-backup',
1650
+ path:transaction.backupPath,
1651
+ message:String(error?.message??error),
1652
+ recovery:'The verified output is committed; inspect and remove this preserved prior artifact.'
1653
+ });
1654
+ }
1655
+ }
1656
+
1657
+ async function resolveOutputTarget(requested,releaseRoot){
1658
+ const requestedParent=path.dirname(requested);
1659
+ const missing=[];
1660
+ let existing=requestedParent;
1661
+ for(;;){
1662
+ try{
1663
+ const info=await lstat(existing);
1664
+ if(info.isSymbolicLink()||!info.isDirectory()){
1665
+ fail('Bundle output parent must resolve through regular directories.',ERROR_CODES.policyDenied);
1666
+ }
1667
+ break;
1668
+ }catch(error){
1669
+ if(error?.code!=='ENOENT')throw error;
1670
+ const parent=path.dirname(existing);
1671
+ if(parent===existing)throw error;
1672
+ missing.unshift(path.basename(existing));
1673
+ existing=parent;
1674
+ }
1675
+ }
1676
+ const canonicalExisting=await realpath(existing);
1677
+ const candidateParent=path.join(canonicalExisting,...missing);
1678
+ if(isInside(releaseRoot,candidateParent)){
1679
+ fail('Bundle output cannot be inside the authenticated release root.',ERROR_CODES.policyDenied);
1680
+ }
1681
+ await mkdir(candidateParent,{recursive:true});
1682
+ const canonicalParent=await realpath(candidateParent);
1683
+ if(isInside(releaseRoot,canonicalParent)){
1684
+ fail('Bundle output parent resolved inside the authenticated release root.',ERROR_CODES.policyDenied);
1685
+ }
1686
+ return path.join(canonicalParent,path.basename(requested));
1687
+ }
1688
+
1689
+ function parseCanonicalJson(bytes,label){
1690
+ let text;
1691
+ try{
1692
+ text=textDecoder.decode(bytes);
1693
+ }catch(error){
1694
+ fail(`${label} is not UTF-8 JSON.`,ERROR_CODES.integrityFailed,{cause:error.message});
1695
+ }
1696
+ let value;
1697
+ try{
1698
+ value=JSON.parse(text);
1699
+ }catch(error){
1700
+ fail(`${label} is not valid JSON.`,ERROR_CODES.integrityFailed,{cause:error.message});
1701
+ }
1702
+ if(!bytes.equals(canonicalJsonBytes(value))){
1703
+ fail(`${label} is not canonical two-space JSON with one trailing LF.`);
1704
+ }
1705
+ return value;
1706
+ }
1707
+
1708
+ class StrictTarParser{
1709
+ constructor(compressedBytes){
1710
+ this.compressedBytes=compressedBytes;
1711
+ this.expansionLimit=Math.min(
1712
+ APP_BUNDLE_LIMITS.maxExpandedBytes,
1713
+ compressedBytes*APP_BUNDLE_LIMITS.maxExpansionRatio
1714
+ +APP_BUNDLE_LIMITS.expansionSlackBytes
1715
+ );
1716
+ this.totalExpanded=0;
1717
+ this.header=Buffer.alloc(TAR_BLOCK_BYTES);
1718
+ this.headerBytes=0;
1719
+ this.state='header';
1720
+ this.zeroBlocks=0;
1721
+ this.entries=[];
1722
+ this.pathTopology={kinds:new Map(),spellings:new Map()};
1723
+ this.current=null;
1724
+ this.remaining=0;
1725
+ this.padding=0;
1726
+ this.ended=false;
1727
+ }
1728
+
1729
+ consume(chunk){
1730
+ this.totalExpanded+=chunk.length;
1731
+ if(this.totalExpanded>this.expansionLimit){
1732
+ fail('Bundle expansion exceeds its absolute or ratio ceiling.',ERROR_CODES.policyDenied);
1733
+ }
1734
+ let offset=0;
1735
+ while(offset<chunk.length){
1736
+ if(this.ended)fail('USTAR archive contains bytes after its two terminal zero blocks.');
1737
+ if(this.state==='header'){
1738
+ const count=Math.min(TAR_BLOCK_BYTES-this.headerBytes,chunk.length-offset);
1739
+ chunk.copy(this.header,this.headerBytes,offset,offset+count);
1740
+ this.headerBytes+=count;
1741
+ offset+=count;
1742
+ if(this.headerBytes===TAR_BLOCK_BYTES)this.finishHeader();
1743
+ continue;
1744
+ }
1745
+ if(this.state==='data'){
1746
+ const count=Math.min(this.remaining,chunk.length-offset);
1747
+ const piece=chunk.subarray(offset,offset+count);
1748
+ this.current.digest.update(piece);
1749
+ if(this.current.chunks){
1750
+ this.current.chunks.push(Buffer.from(piece));
1751
+ this.current.controlBytes+=piece.length;
1752
+ if(this.current.controlBytes>APP_BUNDLE_LIMITS.maxControlBytes){
1753
+ fail(`${this.current.path} exceeds the control-document limit.`,ERROR_CODES.policyDenied);
1754
+ }
1755
+ }
1756
+ this.remaining-=count;
1757
+ offset+=count;
1758
+ if(this.remaining===0)this.finishEntry();
1759
+ continue;
1760
+ }
1761
+ if(this.state==='padding'){
1762
+ const count=Math.min(this.padding,chunk.length-offset);
1763
+ if(chunk.subarray(offset,offset+count).some(byte=>byte!==0)){
1764
+ fail(`USTAR padding is nonzero after ${this.current?.path??'an entry'}.`);
1765
+ }
1766
+ this.padding-=count;
1767
+ offset+=count;
1768
+ if(this.padding===0){
1769
+ this.current=null;
1770
+ this.state='header';
1771
+ }
1772
+ }
1773
+ }
1774
+ }
1775
+
1776
+ finishHeader(){
1777
+ const header=Buffer.from(this.header);
1778
+ this.header.fill(0);
1779
+ this.headerBytes=0;
1780
+ if(header.every(byte=>byte===0)){
1781
+ this.zeroBlocks+=1;
1782
+ if(this.zeroBlocks===2){
1783
+ this.ended=true;
1784
+ return;
1785
+ }
1786
+ this.state='header';
1787
+ return;
1788
+ }
1789
+ if(this.zeroBlocks!==0)fail('USTAR archive resumed after its first terminal zero block.');
1790
+ if(this.entries.length>=APP_BUNDLE_LIMITS.maxEntries){
1791
+ fail('USTAR archive exceeds its entry-count limit.',ERROR_CODES.policyDenied);
1792
+ }
1793
+ const parsed=parseCanonicalUstarHeader(header);
1794
+ const expectedControl=[
1795
+ APP_BUNDLE_MANIFEST_NAME,
1796
+ APP_BUNDLE_DESCRIPTOR_NAME,
1797
+ APP_BUNDLE_RELEASE_PATH
1798
+ ][this.entries.length];
1799
+ if(expectedControl&&parsed.path!==expectedControl){
1800
+ fail(`USTAR entry ${this.entries.length+1} must be ${expectedControl}.`);
1801
+ }
1802
+ if(this.entries.length>=3&&!parsed.path.startsWith('payload/')){
1803
+ fail('Every non-control USTAR entry must be beneath payload/.');
1804
+ }
1805
+ registerPortablePathTopology(this.pathTopology,parsed.path,'USTAR archive topology');
1806
+ this.current={
1807
+ path:parsed.path,
1808
+ size:parsed.size,
1809
+ digest:createHash('sha256'),
1810
+ chunks:CONTROL_PATHS.has(parsed.path)?[]:null,
1811
+ controlBytes:0
1812
+ };
1813
+ this.remaining=parsed.size;
1814
+ this.padding=(TAR_BLOCK_BYTES-(parsed.size%TAR_BLOCK_BYTES))%TAR_BLOCK_BYTES;
1815
+ this.state='data';
1816
+ if(this.remaining===0)this.finishEntry();
1817
+ }
1818
+
1819
+ finishEntry(){
1820
+ const entry=Object.freeze({
1821
+ path:this.current.path,
1822
+ bytes:this.current.size,
1823
+ sha256:this.current.digest.digest('hex'),
1824
+ ...(this.current.chunks?{buffer:Buffer.concat(this.current.chunks)}:{})
1825
+ });
1826
+ this.entries.push(entry);
1827
+ if(this.padding===0){
1828
+ this.current=null;
1829
+ this.state='header';
1830
+ }else{
1831
+ this.state='padding';
1832
+ }
1833
+ }
1834
+
1835
+ finish(){
1836
+ if(!this.ended||this.zeroBlocks!==2||this.headerBytes!==0
1837
+ ||this.state!=='header'||this.current!==null){
1838
+ fail('USTAR archive ended before its exact two-block terminator.');
1839
+ }
1840
+ return Object.freeze({
1841
+ entries:Object.freeze(this.entries),
1842
+ expandedBytes:this.totalExpanded
1843
+ });
1844
+ }
1845
+ }
1846
+
1847
+ class DeterministicGzipDigest{
1848
+ constructor(){
1849
+ this.digest=createHash('sha256');
1850
+ this.digest.update(GZIP_HEADER);
1851
+ this.compressedBytes=GZIP_HEADER.length;
1852
+ this.crc=0xffffffff;
1853
+ this.expandedBytes=0;
1854
+ this.deflate=createDeflateRaw({level:9});
1855
+ this.deflate.on('data',chunk=>{
1856
+ this.digest.update(chunk);
1857
+ this.compressedBytes+=chunk.length;
1858
+ });
1859
+ }
1860
+
1861
+ async write(chunk){
1862
+ this.crc=updateCrc32(this.crc,chunk);
1863
+ this.expandedBytes=(this.expandedBytes+chunk.length)>>>0;
1864
+ if(!this.deflate.write(chunk))await once(this.deflate,'drain');
1865
+ }
1866
+
1867
+ async finish(){
1868
+ const completed=new Promise((resolve,reject)=>{
1869
+ this.deflate.once('end',resolve);
1870
+ this.deflate.once('error',reject);
1871
+ });
1872
+ this.deflate.end();
1873
+ await completed;
1874
+ const trailer=Buffer.alloc(8);
1875
+ trailer.writeUInt32LE((this.crc^0xffffffff)>>>0,0);
1876
+ trailer.writeUInt32LE(this.expandedBytes>>>0,4);
1877
+ this.digest.update(trailer);
1878
+ this.compressedBytes+=trailer.length;
1879
+ return Object.freeze({
1880
+ sha256:this.digest.digest('hex'),
1881
+ compressedBytes:this.compressedBytes
1882
+ });
1883
+ }
1884
+
1885
+ destroy(){
1886
+ this.deflate.destroy();
1887
+ }
1888
+ }
1889
+
1890
+ function validateFileRecord(file,label){
1891
+ assertExactKeys(file,['path','bytes','sha256'],label);
1892
+ validateAppBundlePath(file.path,`${label}.path`);
1893
+ assertInteger(file.bytes,{
1894
+ minimum:0,
1895
+ maximum:APP_BUNDLE_LIMITS.maxEntryBytes,
1896
+ label:`${label}.bytes`
1897
+ });
1898
+ assertSha256(file.sha256,`${label}.sha256`);
1899
+ return Object.freeze({path:file.path,bytes:file.bytes,sha256:file.sha256});
1900
+ }
1901
+
1902
+ function validateBundleManifest(value){
1903
+ assertExactKeys(value,[
1904
+ 'schemaVersion','kind','format','sdk','app','descriptor','release','payload'
1905
+ ],APP_BUNDLE_MANIFEST_NAME);
1906
+ if(value.schemaVersion!==APP_BUNDLE_SCHEMA_VERSION||value.kind!==APP_BUNDLE_KIND
1907
+ ||value.format!==APP_BUNDLE_FORMAT){
1908
+ fail('Bundle manifest protocol discriminator is unsupported.');
1909
+ }
1910
+ assertExactKeys(value.sdk,['name','version'],'bundle.sdk');
1911
+ if(value.sdk.name!==SDK_NAME)fail(`bundle.sdk.name must be ${SDK_NAME}.`);
1912
+ parseSemver(value.sdk.version);
1913
+ if(!APP_BUNDLE_SUPPORTED_SDK_VERSIONS.includes(value.sdk.version)){
1914
+ fail(
1915
+ `bundle.sdk.version ${value.sdk.version} is structurally known but not compatible with this Arcane SDK generation.`,
1916
+ ERROR_CODES.policyDenied
1917
+ );
1918
+ }
1919
+ assertExactKeys(value.app,['id','version'],'bundle.app');
1920
+ if(typeof value.app.id!=='string'||!APP_ID_PATTERN.test(value.app.id))fail('bundle.app.id is invalid.');
1921
+ parseSemver(value.app.version);
1922
+ assertExactKeys(value.descriptor,[
1923
+ 'path','schemaVersion','canonicalSha256','packageSha256','fileSha256','bytes'
1924
+ ],'bundle.descriptor');
1925
+ if(value.descriptor.path!==APP_BUNDLE_DESCRIPTOR_NAME||value.descriptor.schemaVersion!==2){
1926
+ fail('bundle.descriptor must identify the authored schema-2 descriptor.');
1927
+ }
1928
+ for(const field of ['canonicalSha256','packageSha256','fileSha256']){
1929
+ assertSha256(value.descriptor[field],`bundle.descriptor.${field}`);
1930
+ }
1931
+ assertInteger(value.descriptor.bytes,{
1932
+ minimum:1,
1933
+ maximum:APP_BUNDLE_LIMITS.maxControlBytes,
1934
+ label:'bundle.descriptor.bytes'
1935
+ });
1936
+ assertExactKeys(value.release,[
1937
+ 'path','schemaVersion','builder','policySha256','manifestSha256',
1938
+ 'contentSha256','fileCount','totalBytes'
1939
+ ],'bundle.release');
1940
+ if(value.release.path!==APP_BUNDLE_RELEASE_PATH||value.release.schemaVersion!==1
1941
+ ||value.release.builder!==PACKAGER_VERSION){
1942
+ fail('bundle.release identifies an unsupported app release contract.');
1943
+ }
1944
+ for(const field of ['policySha256','manifestSha256','contentSha256']){
1945
+ assertSha256(value.release[field],`bundle.release.${field}`);
1946
+ }
1947
+ assertInteger(value.release.fileCount,{
1948
+ minimum:1,
1949
+ maximum:APP_BUNDLE_LIMITS.maxPayloadFiles,
1950
+ label:'bundle.release.fileCount'
1951
+ });
1952
+ assertInteger(value.release.totalBytes,{
1953
+ minimum:1,
1954
+ maximum:APP_BUNDLE_LIMITS.maxExpandedBytes,
1955
+ label:'bundle.release.totalBytes'
1956
+ });
1957
+ assertExactKeys(value.payload,['root','fileCount','totalBytes','files'],'bundle.payload');
1958
+ if(value.payload.root!=='payload'||!Array.isArray(value.payload.files)){
1959
+ fail('bundle.payload must contain one exact payload inventory.');
1960
+ }
1961
+ if(value.payload.files.length<1||value.payload.files.length>APP_BUNDLE_LIMITS.maxPayloadFiles){
1962
+ fail('bundle.payload.files exceeds its cardinality contract.');
1963
+ }
1964
+ const keys=new Set();
1965
+ let totalBytes=0;
1966
+ let previous=null;
1967
+ const files=value.payload.files.map((file,index)=>{
1968
+ const record=validateFileRecord(file,`bundle.payload.files[${index}]`);
1969
+ if(pathKey(record.path)===pathKey(RELEASE_MANIFEST_NAME)){
1970
+ fail(`bundle.payload.files must not contain ${RELEASE_MANIFEST_NAME}.`);
1971
+ }
1972
+ const key=pathKey(record.path);
1973
+ if(keys.has(key))fail(`Bundle payload has a duplicate or case-colliding path: ${record.path}.`);
1974
+ keys.add(key);
1975
+ if(previous!==null&&compareText(previous,record.path)>=0){
1976
+ fail('Bundle payload inventory must use strict canonical path order.');
1977
+ }
1978
+ previous=record.path;
1979
+ totalBytes+=record.bytes;
1980
+ if(!Number.isSafeInteger(totalBytes)||totalBytes>APP_BUNDLE_LIMITS.maxExpandedBytes){
1981
+ fail('Bundle payload exceeds its expanded byte ceiling.',ERROR_CODES.policyDenied);
1982
+ }
1983
+ return record;
1984
+ });
1985
+ validatePortablePathTopology([
1986
+ APP_BUNDLE_MANIFEST_NAME,
1987
+ APP_BUNDLE_DESCRIPTOR_NAME,
1988
+ APP_BUNDLE_RELEASE_PATH,
1989
+ ...files.map(file=>`payload/${file.path}`)
1990
+ ],'Bundle archive topology');
1991
+ if(value.payload.fileCount!==files.length||value.payload.totalBytes!==totalBytes
1992
+ ||value.release.fileCount!==files.length||value.release.totalBytes!==totalBytes){
1993
+ fail('Bundle payload and release totals do not match their inventory.');
1994
+ }
1995
+ return {manifest:value,files:Object.freeze(files)};
1996
+ }
1997
+
1998
+ function validateEmbeddedRelease(value,descriptor,files,bundle){
1999
+ assertExactKeys(value,[
2000
+ 'schemaVersion','builder','app','policySha256','fileCount','totalBytes','contentSha256','files'
2001
+ ],RELEASE_MANIFEST_NAME);
2002
+ if(value.schemaVersion!==1||value.builder!==PACKAGER_VERSION){
2003
+ fail('Embedded release manifest protocol is unsupported.');
2004
+ }
2005
+ assertExactKeys(value.app,[
2006
+ 'id','displayName','version','entry','start','security','localAIModelPolicy'
2007
+ ],'embedded release app');
2008
+ const expectedApp=expectedReleaseApp(descriptor);
2009
+ if(JSON.stringify(value.app)!==JSON.stringify(expectedApp)){
2010
+ fail('Embedded release app does not match the authored descriptor.');
2011
+ }
2012
+ const releaseFiles=Array.isArray(value.files)
2013
+ ?value.files.map((file,index)=>validateFileRecord(file,`release.files[${index}]`))
2014
+ :null;
2015
+ if(!releaseFiles||JSON.stringify(releaseFiles)!==JSON.stringify(files)){
2016
+ fail('Embedded release inventory does not match the bundle payload inventory.');
2017
+ }
2018
+ const totalBytes=files.reduce((total,file)=>total+file.bytes,0);
2019
+ const contentSha256=sha256(JSON.stringify(files));
2020
+ assertSha256(value.policySha256,'release.policySha256');
2021
+ if(value.fileCount!==files.length||value.totalBytes!==totalBytes
2022
+ ||value.contentSha256!==contentSha256){
2023
+ fail('Embedded release inventory totals or content digest are invalid.');
2024
+ }
2025
+ if(bundle.release.builder!==value.builder
2026
+ ||bundle.release.policySha256!==value.policySha256
2027
+ ||bundle.release.contentSha256!==value.contentSha256
2028
+ ||bundle.release.fileCount!==value.fileCount
2029
+ ||bundle.release.totalBytes!==value.totalBytes){
2030
+ fail('Bundle release binding does not match the embedded release manifest.');
2031
+ }
2032
+ return value;
2033
+ }
2034
+
2035
+ function validateParsedBundle(parsed,{
2036
+ bundleSha256,
2037
+ compressedBytes,
2038
+ bundlePath,
2039
+ artifactIdentity
2040
+ }){
2041
+ const entries=parsed.entries;
2042
+ if(entries.length<4)fail('Bundle must contain three control documents and at least one payload file.');
2043
+ const [bundleEntry,descriptorEntry,releaseEntry]=entries;
2044
+ const {manifest,files}=validateBundleManifest(
2045
+ parseCanonicalJson(bundleEntry.buffer,APP_BUNDLE_MANIFEST_NAME)
2046
+ );
2047
+ const expectedPaths=[
2048
+ APP_BUNDLE_MANIFEST_NAME,
2049
+ APP_BUNDLE_DESCRIPTOR_NAME,
2050
+ APP_BUNDLE_RELEASE_PATH,
2051
+ ...files.map(file=>`payload/${file.path}`)
2052
+ ];
2053
+ if(JSON.stringify(entries.map(entry=>entry.path))!==JSON.stringify(expectedPaths)){
2054
+ fail('Bundle USTAR topology or entry order does not match the manifest.');
2055
+ }
2056
+ const descriptorDocument=parseCanonicalJson(descriptorEntry.buffer,APP_BUNDLE_DESCRIPTOR_NAME);
2057
+ const descriptor=validateAppDescriptor(descriptorDocument,{appId:manifest.app.id});
2058
+ if(!descriptorEntry.buffer.equals(canonicalJsonBytes(descriptor))
2059
+ ||descriptor.version!==manifest.app.version
2060
+ ||descriptor.schemaVersion!==manifest.descriptor.schemaVersion
2061
+ ||descriptorEntry.bytes!==manifest.descriptor.bytes
2062
+ ||descriptorEntry.sha256!==manifest.descriptor.fileSha256
2063
+ ||appDescriptorSha256(descriptor)!==manifest.descriptor.canonicalSha256
2064
+ ||sha256(JSON.stringify(projectPackageManifest(descriptor)))!==manifest.descriptor.packageSha256){
2065
+ fail('Authored descriptor binding does not match the bundle manifest.');
2066
+ }
2067
+ const releaseDocument=parseCanonicalJson(releaseEntry.buffer,RELEASE_MANIFEST_NAME);
2068
+ validateEmbeddedRelease(releaseDocument,descriptor,files,manifest);
2069
+ if(releaseEntry.sha256!==manifest.release.manifestSha256){
2070
+ fail('Embedded release manifest digest does not match the bundle manifest.');
2071
+ }
2072
+ for(let index=0;index<files.length;index+=1){
2073
+ const entry=entries[index+3];
2074
+ const file=files[index];
2075
+ if(entry.bytes!==file.bytes||entry.sha256!==file.sha256){
2076
+ fail(`Payload entry does not match its release identity: ${file.path}.`);
2077
+ }
2078
+ }
2079
+ const consistency=Object.freeze({
2080
+ artifact:Object.freeze({sha256:bundleSha256,bytes:compressedBytes}),
2081
+ descriptor:Object.freeze({
2082
+ canonicalSha256:manifest.descriptor.canonicalSha256,
2083
+ fileSha256:manifest.descriptor.fileSha256,
2084
+ packageSha256:manifest.descriptor.packageSha256,
2085
+ bytes:manifest.descriptor.bytes
2086
+ }),
2087
+ release:Object.freeze({
2088
+ manifestSha256:manifest.release.manifestSha256,
2089
+ policySha256:manifest.release.policySha256,
2090
+ contentSha256:manifest.release.contentSha256,
2091
+ fileCount:manifest.release.fileCount,
2092
+ totalBytes:manifest.release.totalBytes
2093
+ })
2094
+ });
2095
+ return Object.freeze({
2096
+ schemaVersion:APP_BUNDLE_SCHEMA_VERSION,
2097
+ kind:'arcane-app-release-bundle-verification',
2098
+ verified:true,
2099
+ arcaneCompatible:true,
2100
+ bundlePath,
2101
+ bundleSha256,
2102
+ compressedBytes,
2103
+ artifactIdentity,
2104
+ expandedBytes:parsed.expandedBytes,
2105
+ entryCount:entries.length,
2106
+ sdk:Object.freeze({...manifest.sdk}),
2107
+ app:Object.freeze({...manifest.app}),
2108
+ descriptorSha256:manifest.descriptor.canonicalSha256,
2109
+ descriptorFileSha256:manifest.descriptor.fileSha256,
2110
+ descriptorBytes:manifest.descriptor.bytes,
2111
+ packageSha256:manifest.descriptor.packageSha256,
2112
+ releaseManifestSha256:manifest.release.manifestSha256,
2113
+ releasePolicySha256:manifest.release.policySha256,
2114
+ releaseContentSha256:manifest.release.contentSha256,
2115
+ fileCount:manifest.release.fileCount,
2116
+ totalBytes:manifest.release.totalBytes,
2117
+ consistency
2118
+ });
2119
+ }
2120
+
2121
+ export async function verifyAppReleaseBundle({bundlePath,signal,onEvent}={}){
2122
+ throwIfAborted(signal);
2123
+ if(typeof bundlePath!=='string'||!bundlePath.trim()){
2124
+ fail('bundlePath is required to verify an app release bundle.',ERROR_CODES.usage);
2125
+ }
2126
+ const requested=path.resolve(bundlePath);
2127
+ await emit(onEvent,{type:'bundle.verify.started',bundlePath:requested});
2128
+ const opened=await openStableFile(requested,'app release bundle',{
2129
+ maximum:APP_BUNDLE_LIMITS.maxCompressedBytes
2130
+ });
2131
+ let recompressed;
2132
+ try{
2133
+ const canonicalRequested=await realpath(requested);
2134
+ const canonicalInfo=await lstat(canonicalRequested,{bigint:true});
2135
+ if(canonicalInfo.isSymbolicLink()||!canonicalInfo.isFile()||canonicalInfo.nlink!==1n
2136
+ ||!identityMatches(canonicalInfo,opened.identity)){
2137
+ fail('App release bundle canonical path did not bind to its opened identity.');
2138
+ }
2139
+ await emit(onEvent,{
2140
+ type:'bundle.verify.opened',
2141
+ bundlePath:canonicalRequested,
2142
+ compressedBytes:opened.identity.bytes
2143
+ });
2144
+ if(opened.identity.bytes<GZIP_HEADER.length+8){
2145
+ fail('App release bundle is too short to be canonical gzip.');
2146
+ }
2147
+ const header=Buffer.alloc(GZIP_HEADER.length);
2148
+ const first=await opened.handle.read(header,0,header.length,0);
2149
+ if(first.bytesRead!==header.length||!header.equals(GZIP_HEADER)){
2150
+ fail('Bundle gzip header is not the deterministic Arcane header.');
2151
+ }
2152
+ const parser=new StrictTarParser(opened.identity.bytes);
2153
+ recompressed=new DeterministicGzipDigest();
2154
+ const actualDigest=createHash('sha256');
2155
+ let actualBytes=0;
2156
+ const actual=new Transform({
2157
+ transform(chunk,_encoding,callback){
2158
+ try{
2159
+ actualBytes+=chunk.length;
2160
+ if(actualBytes>opened.identity.bytes
2161
+ ||actualBytes>APP_BUNDLE_LIMITS.maxCompressedBytes){
2162
+ fail('Bundle compressed stream exceeded its recorded byte ceiling.',ERROR_CODES.policyDenied);
2163
+ }
2164
+ actualDigest.update(chunk);
2165
+ callback(null,chunk);
2166
+ }catch(error){
2167
+ callback(error);
2168
+ }
2169
+ }
2170
+ });
2171
+ const gunzip=createGunzip();
2172
+ const consume=new Writable({
2173
+ write(chunk,_encoding,callback){
2174
+ try{
2175
+ throwIfAborted(signal);
2176
+ parser.consume(chunk);
2177
+ recompressed.write(chunk).then(()=>callback(),callback);
2178
+ }catch(error){
2179
+ callback(error);
2180
+ }
2181
+ }
2182
+ });
2183
+ await pipeline(
2184
+ opened.handle.createReadStream({
2185
+ autoClose:false,
2186
+ start:0,
2187
+ end:opened.identity.bytes-1
2188
+ }),
2189
+ actual,
2190
+ gunzip,
2191
+ consume,
2192
+ {signal}
2193
+ );
2194
+ const parsed=parser.finish();
2195
+ const deterministic=await recompressed.finish();
2196
+ const bundleSha256=actualDigest.digest('hex');
2197
+ const probe=Buffer.alloc(1);
2198
+ const extra=await opened.handle.read(probe,0,1,opened.identity.bytes);
2199
+ if(extra.bytesRead!==0)fail('App release bundle grew while it was verified.');
2200
+ if(actualBytes!==opened.identity.bytes
2201
+ ||deterministic.compressedBytes!==actualBytes
2202
+ ||deterministic.sha256!==bundleSha256){
2203
+ fail('Bundle gzip member is not the exact deterministic Arcane encoding.');
2204
+ }
2205
+ const canonicalCurrent=await realpath(requested);
2206
+ const after=await opened.handle.stat({bigint:true});
2207
+ const current=await lstat(requested,{bigint:true});
2208
+ const canonicalAfter=await lstat(canonicalRequested,{bigint:true});
2209
+ if(after.nlink!==1n||current.nlink!==1n||canonicalAfter.nlink!==1n
2210
+ ||!identityMatches(after,opened.identity)||current.isSymbolicLink()
2211
+ ||canonicalAfter.isSymbolicLink()||!current.isFile()||!canonicalAfter.isFile()
2212
+ ||!identityMatches(current,opened.identity)
2213
+ ||!identityMatches(canonicalAfter,opened.identity)
2214
+ ||canonicalCurrent!==canonicalRequested){
2215
+ fail('App release bundle changed while it was verified.');
2216
+ }
2217
+ const receipt=validateParsedBundle(parsed,{
2218
+ bundleSha256,
2219
+ compressedBytes:actualBytes,
2220
+ bundlePath:canonicalRequested,
2221
+ artifactIdentity:opened.identity
2222
+ });
2223
+ await emit(onEvent,{
2224
+ type:'bundle.verify.completed',
2225
+ bundlePath:receipt.bundlePath,
2226
+ bundleSha256:receipt.bundleSha256,
2227
+ entryCount:receipt.entryCount
2228
+ });
2229
+ return receipt;
2230
+ }catch(error){
2231
+ recompressed?.destroy();
2232
+ if(error?.name==='AbortError'||signal?.aborted)throwIfAborted(signal);
2233
+ throw error;
2234
+ }finally{
2235
+ await opened.handle.close().catch(()=>{});
2236
+ }
2237
+ }
2238
+
2239
+ export async function createAppReleaseBundle({
2240
+ receipt,
2241
+ releaseRoot,
2242
+ outputPath,
2243
+ overwrite=false,
2244
+ signal,
2245
+ onEvent
2246
+ }={}){
2247
+ throwIfAborted(signal);
2248
+ if(typeof overwrite!=='boolean'){
2249
+ fail('overwrite must be a literal boolean.',ERROR_CODES.usage);
2250
+ }
2251
+ if(typeof releaseRoot!=='string'||!releaseRoot.trim()){
2252
+ fail('releaseRoot is required to create an app release bundle.',ERROR_CODES.usage);
2253
+ }
2254
+ if(typeof outputPath!=='string'||!outputPath.trim()){
2255
+ fail('outputPath is required to create an app release bundle.',ERROR_CODES.usage);
2256
+ }
2257
+ const requestedOutput=path.resolve(outputPath);
2258
+ validateOutputFilename(requestedOutput);
2259
+ const authority=await authenticateAppReleaseAuthority(receipt,{releaseRoot,signal});
2260
+ const canonicalReleaseRoot=await realpath(path.resolve(releaseRoot));
2261
+ const descriptor=validateAppDescriptor(authority.descriptor,{appId:receipt.app?.id});
2262
+ if(authority.source!=='authored'||descriptor.schemaVersion!==2
2263
+ ||authority.descriptorSha256!==appDescriptorSha256(descriptor)){
2264
+ fail('Bundle creation requires one authenticated authored schema-2 descriptor.');
2265
+ }
2266
+ const files=validateReleaseInventory(receipt);
2267
+ const release=releaseDocumentFromReceipt(receipt,descriptor,files);
2268
+ const releaseBytes=canonicalJsonBytes(release);
2269
+ const sourceReleaseBytes=await readStableControlFile(
2270
+ path.join(canonicalReleaseRoot,RELEASE_MANIFEST_NAME),
2271
+ RELEASE_MANIFEST_NAME,
2272
+ {signal}
2273
+ );
2274
+ if(!sourceReleaseBytes.equals(releaseBytes)){
2275
+ fail('Authenticated release manifest bytes are not the canonical receipt projection.');
2276
+ }
2277
+ const descriptorBytes=canonicalJsonBytes(descriptor);
2278
+ if(descriptorBytes.length>APP_BUNDLE_LIMITS.maxControlBytes){
2279
+ fail('Authored app descriptor exceeds the control-document limit.',ERROR_CODES.policyDenied);
2280
+ }
2281
+ const manifest=createBundleManifest({
2282
+ descriptor,
2283
+ descriptorBytes,
2284
+ descriptorSha256:authority.descriptorSha256,
2285
+ release,
2286
+ releaseBytes
2287
+ });
2288
+ const manifestBytes=canonicalJsonBytes(manifest);
2289
+ if(manifestBytes.length>APP_BUNDLE_LIMITS.maxControlBytes){
2290
+ fail('Bundle manifest exceeds the control-document limit.',ERROR_CODES.policyDenied);
2291
+ }
2292
+ const entries=[
2293
+ {path:APP_BUNDLE_MANIFEST_NAME,bytes:manifestBytes.length,buffer:manifestBytes},
2294
+ {path:APP_BUNDLE_DESCRIPTOR_NAME,bytes:descriptorBytes.length,buffer:descriptorBytes},
2295
+ {path:APP_BUNDLE_RELEASE_PATH,bytes:releaseBytes.length,buffer:releaseBytes},
2296
+ ...files.map(identity=>({
2297
+ path:validateAppBundlePath(`payload/${identity.path}`,'bundle payload archive path'),
2298
+ bytes:identity.bytes,
2299
+ identity
2300
+ }))
2301
+ ];
2302
+ validatePortablePathTopology(entries.map(entry=>entry.path),'Bundle archive topology');
2303
+ if(entries.length>APP_BUNDLE_LIMITS.maxEntries){
2304
+ fail('Bundle exceeds the archive entry-count limit.',ERROR_CODES.policyDenied);
2305
+ }
2306
+ const output=await resolveOutputTarget(requestedOutput,canonicalReleaseRoot);
2307
+ const token=`${process.pid}-${Date.now()}-${randomBytes(6).toString('hex')}`;
2308
+ const temporary=path.join(path.dirname(output),`.${path.basename(output)}.${token}.tmp`);
2309
+ const releaseLock=await acquireArtifactLock(output,{onEvent});
2310
+ let handle;
2311
+ let existingOutput;
2312
+ let temporaryIdentity;
2313
+ const stagingState={
2314
+ identity:null,
2315
+ contentBytes:0,
2316
+ contentSha256:sha256(Buffer.alloc(0))
2317
+ };
2318
+ let committed=false;
2319
+ let promotion;
2320
+ let result;
2321
+ let operationError;
2322
+ const cleanupIssues=[];
2323
+ try{
2324
+ existingOutput=await inspectOutput(output,{overwrite,signal});
2325
+ await emit(onEvent,{
2326
+ type:'bundle.archive.started',
2327
+ outputPath:output,
2328
+ appId:descriptor.id,
2329
+ version:descriptor.version,
2330
+ entryCount:entries.length
2331
+ });
2332
+ handle=await open(temporary,'wx+',0o600);
2333
+ const createdTemporary=await handle.stat({bigint:true});
2334
+ if(!createdTemporary.isFile()||createdTemporary.nlink!==1n){
2335
+ fail('New bundle staging is not an owned single-link regular file.',ERROR_CODES.policyDenied);
2336
+ }
2337
+ const encoded=await writeDeterministicGzip(handle,entries,{
2338
+ releaseRoot:canonicalReleaseRoot,
2339
+ signal,
2340
+ onEvent,
2341
+ stagingState
2342
+ });
2343
+ if(stagingState.contentSha256!==encoded.sha256
2344
+ ||stagingState.contentBytes!==encoded.bytes){
2345
+ fail('Archive writer did not retain its encoded content identity.');
2346
+ }
2347
+ await handle.chmod(ARCHIVE_MODE);
2348
+ await handle.sync();
2349
+ const encodedTemporary=await handle.stat({bigint:true});
2350
+ if(!encodedTemporary.isFile()||encodedTemporary.nlink!==1n){
2351
+ fail('Encoded bundle staging is not the originally created file object.',ERROR_CODES.policyDenied);
2352
+ }
2353
+ temporaryIdentity=fileIdentity(encodedTemporary);
2354
+ stagingState.identity=temporaryIdentity;
2355
+ throwIfAborted(signal);
2356
+ await authenticateAppReleaseAuthority(receipt,{releaseRoot:canonicalReleaseRoot,signal});
2357
+ const verified=await verifyAppReleaseBundle({bundlePath:temporary,signal});
2358
+ if(verified.bundleSha256!==encoded.sha256||verified.compressedBytes!==encoded.bytes){
2359
+ fail('New bundle verification did not reproduce its encoded identity.');
2360
+ }
2361
+ const temporaryInfo=await lstat(temporary,{bigint:true});
2362
+ if(temporaryInfo.isSymbolicLink()||!temporaryInfo.isFile()||temporaryInfo.nlink!==1n){
2363
+ fail('Verified bundle staging is not a regular file.',ERROR_CODES.policyDenied);
2364
+ }
2365
+ temporaryIdentity=fileIdentity(temporaryInfo);
2366
+ const anchoredTemporary=await handle.stat({bigint:true});
2367
+ if(!anchoredTemporary.isFile()||anchoredTemporary.nlink!==1n
2368
+ ||!identityMatches(anchoredTemporary,temporaryIdentity)){
2369
+ fail('Verified bundle staging is not the originally created file object.',ERROR_CODES.policyDenied);
2370
+ }
2371
+ if(!identitiesEqual(temporaryIdentity,verified.artifactIdentity)){
2372
+ fail('Verified bundle staging changed after independent verification.');
2373
+ }
2374
+ stagingState.identity=temporaryIdentity;
2375
+ stagingState.contentBytes=verified.compressedBytes;
2376
+ stagingState.contentSha256=verified.bundleSha256;
2377
+ await emit(onEvent,{
2378
+ type:'bundle.archive.verified',
2379
+ bundleSha256:verified.bundleSha256,
2380
+ compressedBytes:verified.compressedBytes
2381
+ });
2382
+ throwIfAborted(signal);
2383
+ promotion=await promoteArtifact(temporary,output,{
2384
+ existingOutput,
2385
+ stagingState,
2386
+ anchorHandle:handle,
2387
+ onEvent,
2388
+ signal
2389
+ });
2390
+ await emit(onEvent,{
2391
+ type:'bundle.archive.promoted',
2392
+ bundlePath:output,
2393
+ bundleSha256:verified.bundleSha256
2394
+ });
2395
+ const promoted=await stableFileDigest(output,'promoted app release bundle',{signal});
2396
+ if(promoted.sha256!==verified.bundleSha256
2397
+ ||promoted.bytes!==verified.compressedBytes){
2398
+ fail('Promoted bundle identity does not match the independently verified staging bytes.');
2399
+ }
2400
+ const anchoredPromoted=await handle.stat({bigint:true});
2401
+ if(!anchoredPromoted.isFile()||anchoredPromoted.nlink!==1n
2402
+ ||!identityMatches(anchoredPromoted,promoted.identity)){
2403
+ fail('Promoted bundle digest was not read from the anchored staging object.');
2404
+ }
2405
+ throwIfAborted(signal);
2406
+ const artifactReceipt=Object.freeze({
2407
+ ...verified,
2408
+ kind:'arcane-app-release-bundle-artifact',
2409
+ bundlePath:promoted.path,
2410
+ artifactIdentity:promoted.identity
2411
+ });
2412
+ await handle.close();
2413
+ handle=null;
2414
+ committed=true;
2415
+ const backupCleanup=await finalizePromotion(promotion,{
2416
+ backupHandle:existingOutput?.handle
2417
+ });
2418
+ if(backupCleanup)cleanupIssues.push(backupCleanup);
2419
+ let eventDelivery;
2420
+ try{
2421
+ await emit(onEvent,{
2422
+ type:'bundle.committed',
2423
+ phase:'publish',
2424
+ status:'completed',
2425
+ bundlePath:artifactReceipt.bundlePath,
2426
+ bundleSha256:artifactReceipt.bundleSha256
2427
+ });
2428
+ }catch(error){
2429
+ eventDelivery=Object.freeze({
2430
+ status:'degraded',
2431
+ errorCode:'ARCANE_EVENT_DELIVERY_FAILED',
2432
+ message:String(error?.message??error)
2433
+ });
2434
+ }
2435
+ result={
2436
+ app:descriptor.id,
2437
+ version:descriptor.version,
2438
+ outputPath:artifactReceipt.bundlePath,
2439
+ bundleSha256:artifactReceipt.bundleSha256,
2440
+ compressedBytes:artifactReceipt.compressedBytes,
2441
+ entryCount:artifactReceipt.entryCount,
2442
+ artifactReceipt,
2443
+ ...(eventDelivery?{eventDelivery}:{})
2444
+ };
2445
+ }catch(error){
2446
+ operationError=error;
2447
+ if(promotion&&!committed){
2448
+ const rollbackIssues=await rollbackPromotion(promotion,{
2449
+ promotedHandle:handle,
2450
+ backupHandle:existingOutput?.handle
2451
+ });
2452
+ if(rollbackIssues.length){
2453
+ appendErrorWarning(error,`Rollback warning: ${rollbackIssues.join('; ')}`);
2454
+ }
2455
+ }
2456
+ }finally{
2457
+ let stagingCleanupHandled=false;
2458
+ let retryStagingIdentity=null;
2459
+ if(!committed&&handle){
2460
+ const stagingCleanup=await cleanupAnchoredTemporary(
2461
+ temporary,
2462
+ handle,
2463
+ stagingState?.identity??temporaryIdentity,
2464
+ stagingState.contentSha256,
2465
+ stagingState.contentBytes
2466
+ );
2467
+ if(stagingCleanup.issue)cleanupIssues.push(stagingCleanup.issue);
2468
+ retryStagingIdentity=stagingCleanup.retryIdentity;
2469
+ stagingCleanupHandled=true;
2470
+ }
2471
+ if(handle){
2472
+ try{
2473
+ await handle.close();
2474
+ }catch(error){
2475
+ cleanupIssues.push(changedStagingCleanupIssue(
2476
+ temporary,
2477
+ `Staging creation handle close warning for ${temporary}: ${String(error?.message??error)}`
2478
+ ));
2479
+ }
2480
+ handle=null;
2481
+ }
2482
+ if(!committed&&(!stagingCleanupHandled||retryStagingIdentity)){
2483
+ const stagingCleanup=await cleanupOwnedTemporary(
2484
+ temporary,
2485
+ retryStagingIdentity??stagingState?.identity??temporaryIdentity,
2486
+ stagingState.contentSha256,
2487
+ stagingState.contentBytes
2488
+ );
2489
+ if(stagingCleanup)cleanupIssues.push(stagingCleanup);
2490
+ }
2491
+ if(existingOutput?.handle){
2492
+ try{
2493
+ await existingOutput.handle.close();
2494
+ }catch(error){
2495
+ cleanupIssues.push(Object.freeze({
2496
+ scope:'artifact-backup-handle',
2497
+ path:promotion?.backupPath??output,
2498
+ message:`Prior-output anchor close warning: ${String(error?.message??error)}`,
2499
+ recovery:'Inspect the preserved prior-output path before removing it.'
2500
+ }));
2501
+ }
2502
+ existingOutput.handle=null;
2503
+ }
2504
+ const lockCleanup=await releaseLock();
2505
+ if(lockCleanup)cleanupIssues.push(lockCleanup);
2506
+ }
2507
+ if(operationError){
2508
+ if(cleanupIssues.length){
2509
+ appendErrorWarning(
2510
+ operationError,
2511
+ `Cleanup warning: ${cleanupIssues.map(issue=>issue.message).join('; ')}`
2512
+ );
2513
+ }
2514
+ throw operationError;
2515
+ }
2516
+ if(cleanupIssues.length){
2517
+ result.cleanup=Object.freeze({
2518
+ status:'degraded',
2519
+ issues:Object.freeze(cleanupIssues)
2520
+ });
2521
+ }
2522
+ return Object.freeze(result);
2523
+ }