arcane-os 0.54.1 → 0.55.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,43 @@
1
+ import {readFile} from 'node:fs/promises';
2
+
3
+ /**
4
+ * Generate the native host's classic script from the canonical ESM client.
5
+ * The two owned modules have declaration exports and two explicit imports;
6
+ * this projection is deliberately not a general JavaScript bundler.
7
+ */
8
+ export async function createCoreClassicSource({eventOwnerModuleURL}={}){
9
+ if(typeof eventOwnerModuleURL!=='string'||!eventOwnerModuleURL){
10
+ throw new TypeError('The classic Core client needs the served SDK event-manager module URL.');
11
+ }
12
+ const [contracts,client]=await Promise.all([
13
+ readFile(new URL('../browser-runtime/core/contracts.mjs',import.meta.url),'utf8'),
14
+ readFile(new URL('../browser-runtime/core/client.mjs',import.meta.url),'utf8')
15
+ ]);
16
+ const declarations=contracts.replace(/^export (?=(?:const|class|function) )/gmu,'');
17
+ const implementation=client
18
+ .replace("import {arcaneEvents} from '../event-manager.mjs';",'')
19
+ .replace("import {CORE_PROTOCOL,CORE_READY_EVENTS,CoreError,serializeCoreError} from './contracts.mjs';",'')
20
+ .replace(/^export (?=function )/gmu,'');
21
+ return `(function installArcaneCoreClassic(global){
22
+ 'use strict';
23
+ const arcaneEvents=global.arcaneEvents??null;
24
+ ${declarations}
25
+ ${implementation}
26
+ if(global[CORE_CLIENT_KEY])return;
27
+ function loadSharedEventOwner(){
28
+ return import(${JSON.stringify(eventOwnerModuleURL)}).then(function sharedEventOwner(namespace){return namespace.arcaneEvents;});
29
+ }
30
+ // Document-created native injection precedes the page's managed import map.
31
+ // Only the module import waits for parsing; facade/RPC setup below is immediate.
32
+ const eventOwnerReady=arcaneEvents?Promise.resolve(arcaneEvents)
33
+ :global.document?.readyState==='loading'
34
+ ?new Promise(function afterImportMap(resolve,reject){
35
+ global.document.addEventListener('DOMContentLoaded',function connectSharedEvents(){
36
+ loadSharedEventOwner().then(resolve,reject);
37
+ },{once:true});
38
+ })
39
+ :loadSharedEventOwner();
40
+ installCoreClient(global,{eventOwner:arcaneEvents,eventOwnerReady});
41
+ })(globalThis);
42
+ `;
43
+ }
@@ -42,6 +42,8 @@ const STATIC_RUNTIME_PACKAGE_IMPORTS=new Map([
42
42
  const SDK_BROWSER_SELF_IMPORTS=new Map([
43
43
  ['arcane-os/event-manager',SDK_BROWSER_ENTRY],
44
44
  ['arcane-os/websocket-client','sdk/websocket-client.mjs'],
45
+ ['arcane-os/core/client','sdk/core/client.mjs'],
46
+ ['arcane-os/core/contracts','sdk/core/contracts.mjs'],
45
47
  ['arcane-os/logging','sdk/logging.mjs'],
46
48
  ['arcane-os/browser-device','sdk/browser-device.mjs'],
47
49
  ['arcane-os/pwa','sdk/pwa.mjs'],
@@ -2087,6 +2089,66 @@ function scanHtmlStructure(html){
2087
2089
  return {scripts,links,bases,metas,elements,styles,headClose,bodyClose};
2088
2090
  }
2089
2091
 
2092
+ /** Remove generated PWA references from a selected output document without changing authored content. */
2093
+ export function removePwaEntryReferences(html, {documentUrl, manifestUrl} = {}) {
2094
+ if (!is.string(html)) throw new TypeError('PWA entry HTML must be a string.');
2095
+ const structure = scanHtmlStructure(html);
2096
+ const manifest = new URL(manifestUrl, documentUrl);
2097
+ const base = structure.bases.find(
2098
+ function baseWithHref(element) {
2099
+ return parseTagAttributes(element.open).has('href');
2100
+ }
2101
+ );
2102
+ const baseHref = base ? structuralAttribute(parseTagAttributes(base.open), 'href', 'base') : null;
2103
+ const resourceBase = baseHref ? new URL(baseHref, documentUrl) : documentUrl;
2104
+ const edits = [];
2105
+ for (const link of structure.links) {
2106
+ const attributes = parseTagAttributes(link.open);
2107
+ const href = htmlAttributeView(attributes.get('href') ?? '');
2108
+ const rel = htmlAttributeView(attributes.get('rel') ?? '');
2109
+ if (!href?.decoded || !rel) continue;
2110
+ let target;
2111
+ try {
2112
+ target = new URL(href.decoded, resourceBase);
2113
+ } catch (error) {
2114
+ // An unrelated non-URL reference stays under its authored owner.
2115
+ continue;
2116
+ }
2117
+ if (target.origin !== manifest.origin || target.pathname !== manifest.pathname) continue;
2118
+ const tokens = [...rel.decoded.matchAll(/[^\t\n\f\r ]+/gu)];
2119
+ const manifestTokens = tokens.filter(
2120
+ function manifestRelationship(token) {
2121
+ return asciiLower(token[0]) === 'manifest';
2122
+ }
2123
+ );
2124
+ if (manifestTokens.length === 0) continue;
2125
+ if (manifestTokens.length === tokens.length) {
2126
+ edits.push(
2127
+ {start: link.start, end: link.end, value: ''}
2128
+ );
2129
+ continue;
2130
+ }
2131
+ const position = attributes.positions.get('rel');
2132
+ for (const token of manifestTokens) {
2133
+ edits.push(
2134
+ {
2135
+ start: link.start + position.start + rel.positions[token.index],
2136
+ end: link.start + position.start + rel.positions[token.index + token[0].length],
2137
+ value: ''
2138
+ }
2139
+ );
2140
+ }
2141
+ }
2142
+ for (const script of structure.scripts) {
2143
+ if (parseTagAttributes(script.open).has('data-arcane-pwa')) {
2144
+ edits.push(
2145
+ {start: script.start, end: script.end, value: ''}
2146
+ );
2147
+ }
2148
+ }
2149
+ return applyReferenceEdits(html, edits);
2150
+ }
2151
+
2090
2152
  /** Update only active PWA entry references; registration never orders application startup. */
2091
2153
  export function applyPwaEntryReferences(html,{manifestUrl,bootstrapUrl}={}){
2092
2154
  if(!is.string(html))throw new TypeError('PWA entry HTML must be a string.');
@@ -0,0 +1,184 @@
1
+ import {mkdir, readFile, readdir, realpath, stat, writeFile} from 'node:fs/promises';
2
+ import {createRequire} from 'node:module';
3
+ import path from 'node:path';
4
+ import {fileURLToPath} from 'node:url';
5
+ import Is from 'strong-type';
6
+ import {ArcaneError, ERROR_CODES, throwIfAborted} from '../errors.mjs';
7
+
8
+ const is = new Is(false);
9
+ export const SDK_ROOT = fileURLToPath(new URL('../../', import.meta.url));
10
+ export const CORE_FILES = [
11
+ 'src/core/host.mjs',
12
+ 'src/core/runtime.mjs',
13
+ 'src/core/stdio.mjs',
14
+ 'src/event-manager.mjs',
15
+ 'src/dom-event-instrumentation.mjs',
16
+ 'browser-runtime/core/contracts.mjs',
17
+ 'LICENSE'
18
+ ];
19
+
20
+ export function outputFile(root, relative) {
21
+ if (!is.string(relative) || !relative || path.isAbsolute(relative)) {
22
+ throw new ArcaneError(ERROR_CODES.usage, 'A selected output file needs a relative path.');
23
+ }
24
+ const destination = path.resolve(root, relative);
25
+ const within = path.relative(root, destination);
26
+ if (!within || within === '..' || within.startsWith(`..${path.sep}`) || path.isAbsolute(within)) {
27
+ throw new ArcaneError(ERROR_CODES.usage, `The selected file leaves its output directory: ${relative}.`);
28
+ }
29
+ return destination;
30
+ }
31
+
32
+ export async function writeOutput(root, relative, content, files, signal) {
33
+ throwIfAborted(signal);
34
+ const destination = outputFile(root, relative);
35
+ await mkdir(path.dirname(destination), {recursive: true});
36
+ await writeFile(destination, content, {flag: 'wx', signal});
37
+ files.push(relative.split(path.sep).join('/'));
38
+ }
39
+
40
+ export async function copyRelease(selection, destination, root, files, signal) {
41
+ for (const relative of selection.release.files) {
42
+ throwIfAborted(signal);
43
+ // The supplied reader owns release selection. No text decoding, URL
44
+ // rewriting or line-ending conversion occurs at native assembly.
45
+ const content = is.function(selection.readReleaseFile)
46
+ ? await selection.readReleaseFile(relative, {signal})
47
+ : await readFile(outputFile(selection.releaseRoot, relative), {signal});
48
+ outputFile(path.join(root, destination), relative);
49
+ await writeOutput(root, `${destination}/${relative}`, content, files, signal);
50
+ }
51
+ }
52
+
53
+ async function copyPackageTree(source, relative, root, files, signal, preserved) {
54
+ throwIfAborted(signal);
55
+ const sourceRoot = await realpath(source);
56
+ const outputRelative = path.relative(sourceRoot, root);
57
+ if (!outputRelative || (!path.isAbsolute(outputRelative)
58
+ && outputRelative !== '..' && !outputRelative.startsWith(`..${path.sep}`))) {
59
+ throw new ArcaneError(ERROR_CODES.usage, `Native output would copy itself from ${sourceRoot}.`);
60
+ }
61
+ const entries = await readdir(source, {withFileTypes: true});
62
+ for (const entry of entries) {
63
+ throwIfAborted(signal);
64
+ const selected = path.join(source, entry.name);
65
+ const destination = `${relative}/${entry.name}`;
66
+ if (preserved.has(destination)) continue;
67
+ const info = await stat(selected);
68
+ if (info.isDirectory()) {
69
+ await copyPackageTree(selected, destination, root, files, signal, preserved);
70
+ } else if (info.isFile()) {
71
+ await writeOutput(root, destination, await readFile(selected, {signal}), files, signal);
72
+ } else {
73
+ throw new ArcaneError(ERROR_CODES.operationFailed, `Cannot copy the runtime package entry: ${selected}.`);
74
+ }
75
+ }
76
+ }
77
+
78
+ export async function copyCoreRuntime(root, files, signal, {
79
+ sourceRoot = SDK_ROOT, sdkDestination = 'node_modules/arcane-os'
80
+ } = {}) {
81
+ const metadata = JSON.parse(await readFile(path.join(sourceRoot, 'package.json'), 'utf8'));
82
+ // A direct browser projection may already occupy this package directory.
83
+ // Complete it from its selected installed package without replacing content.
84
+ const preserved = new Set(files);
85
+ for (const directory of ['bin', 'src', 'browser-runtime', 'runtime', 'schemas']) {
86
+ await copyPackageTree(path.join(sourceRoot, directory), `${sdkDestination}/${directory}`, root, files, signal, preserved);
87
+ }
88
+ for (const relative of ['package.json', 'LICENSE', 'COMMERCIAL-LICENSE.md', 'NOTICE', 'README.md', 'CHANGELOG.md']) {
89
+ const destination = `${sdkDestination}/${relative}`;
90
+ if (!preserved.has(destination)) {
91
+ await writeOutput(root, destination, await readFile(path.join(sourceRoot, relative), {signal}), files, signal);
92
+ }
93
+ }
94
+
95
+ // Preserve each dependency's actual installed runtime resolution, including
96
+ // nested versions. Packaging reads published files; it executes no package.
97
+ const copied = new Map([[sdkDestination, await realpath(sourceRoot)]]);
98
+ async function includeDependency(name, from, parentDestination) {
99
+ throwIfAborted(signal);
100
+ const require = createRequire(path.join(from, 'package.json'));
101
+ let packageFile;
102
+ try {
103
+ packageFile = require.resolve(`${name}/package.json`);
104
+ } catch (error) {
105
+ if (error.code !== 'ERR_PACKAGE_PATH_NOT_EXPORTED') throw error;
106
+ let directory = path.dirname(require.resolve(name));
107
+ for (;;) {
108
+ const candidate = path.join(directory, 'package.json');
109
+ try {
110
+ const record = JSON.parse(await readFile(candidate, 'utf8'));
111
+ if (record.name === name) { packageFile = candidate; break; }
112
+ } catch (failure) {
113
+ if (failure.code !== 'ENOENT') throw failure;
114
+ }
115
+ const parent = path.dirname(directory);
116
+ if (parent === directory) throw error;
117
+ directory = parent;
118
+ }
119
+ }
120
+ const source = await realpath(path.dirname(packageFile));
121
+ // Reuse only a resolved ancestor package that Node can actually reach.
122
+ // This also terminates ordinary circular package dependencies.
123
+ let ancestor = parentDestination;
124
+ for (;;) {
125
+ const candidate = path.posix.join(ancestor, 'node_modules', name);
126
+ if (copied.has(candidate)) {
127
+ if (copied.get(candidate) === source) return;
128
+ break;
129
+ }
130
+ try { await stat(outputFile(root, `${candidate}/package.json`)); break; }
131
+ catch (error) { if (error.code !== 'ENOENT') throw error; }
132
+ if (ancestor === '.') break;
133
+ ancestor = path.posix.dirname(ancestor);
134
+ }
135
+ const destination = `${parentDestination}/node_modules/${name}`;
136
+ const record = JSON.parse(await readFile(packageFile, 'utf8'));
137
+ let included = false;
138
+ try { await stat(outputFile(root, `${destination}/package.json`)); included = true; }
139
+ catch (error) { if (error.code !== 'ENOENT') throw error; }
140
+ if (!included) await copyPackageTree(source, destination, root, files, signal, preserved);
141
+ copied.set(destination, source);
142
+ for (const dependency of Object.keys(record.dependencies ?? {})) {
143
+ await includeDependency(dependency, source, destination);
144
+ }
145
+ }
146
+ for (const dependency of Object.keys(metadata.dependencies ?? {})) {
147
+ await includeDependency(dependency, sourceRoot, sdkDestination);
148
+ }
149
+ return metadata;
150
+ }
151
+
152
+ export function coreEntrySource(application, version, services) {
153
+ const imports = services.map(function serviceImport(service, index) {
154
+ const specifier = '../app/' + service.module.split('/').map(encodeURIComponent).join('/');
155
+ return `import createService${index} from ${JSON.stringify(specifier)};`;
156
+ });
157
+ const definitions = services.map(function serviceDefinition(service, index) {
158
+ return ` createService${index}(${JSON.stringify(service.options === undefined ? {} : service.options)})`;
159
+ });
160
+ return [
161
+ "import {startCoreHost} from 'arcane-os/core/host';",
162
+ ...imports,
163
+ '',
164
+ 'const host = startCoreHost({',
165
+ ` application: ${JSON.stringify(application)},`,
166
+ ` version: ${JSON.stringify(version)},`,
167
+ ` services: [\n${definitions.join(',\n')}\n ]`,
168
+ '});',
169
+ '',
170
+ 'function closeCore() { host.close(); }',
171
+ "process.on('SIGINT', closeCore);",
172
+ "process.on('SIGTERM', closeCore);",
173
+ 'try {',
174
+ ' await host.closed;',
175
+ '} catch (error) {',
176
+ " console.error('Arcane Core host failed:', error);",
177
+ ' process.exitCode = 1;',
178
+ '} finally {',
179
+ " process.off('SIGINT', closeCore);",
180
+ " process.off('SIGTERM', closeCore);",
181
+ '}',
182
+ ''
183
+ ].join('\n');
184
+ }
@@ -0,0 +1,211 @@
1
+ import {mkdir, mkdtemp, readFile, realpath, rm, stat} from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import Is from 'strong-type';
4
+ import {createCoreClassicSource} from '../core-classic-source.mjs';
5
+ import {ArcaneError, ERROR_CODES, throwIfAborted} from '../errors.mjs';
6
+ import {SDK_ROOT, CORE_FILES, outputFile, writeOutput, copyRelease, copyCoreRuntime, coreEntrySource} from './portable-layout.mjs';
7
+
8
+ const is = new Is(false);
9
+ const PROTOCOL = 'arcane-native-builder/1';
10
+ const MANIFEST = 'arcane-native.json';
11
+
12
+ function portableTarget(value) {
13
+ if (value?.target !== 'portable') {
14
+ throw new ArcaneError(ERROR_CODES.targetUnavailable, 'This provider assembles portable Core payloads only.');
15
+ }
16
+ return {...value};
17
+ }
18
+
19
+ async function emit(onEvent, event) {
20
+ if (is.function(onEvent)) await onEvent(event);
21
+ }
22
+
23
+ function contains(root, candidate) {
24
+ const relative = path.relative(root, candidate);
25
+ return relative === '' || (!path.isAbsolute(relative) && relative !== '..' && !relative.startsWith(`..${path.sep}`));
26
+ }
27
+
28
+ async function futureRealPath(location) {
29
+ try { return await realpath(location); }
30
+ catch (error) {
31
+ if (error.code !== 'ENOENT') throw error;
32
+ const parent = path.dirname(location);
33
+ if (parent === location) throw error;
34
+ return path.join(await futureRealPath(parent), path.basename(location));
35
+ }
36
+ }
37
+
38
+ async function outputDirectory(request) {
39
+ if (!is.string(request.outputRoot) || !request.outputRoot) {
40
+ throw new ArcaneError(ERROR_CODES.usage, 'Portable assembly requires an outputRoot.');
41
+ }
42
+ const output = await futureRealPath(path.resolve(request.outputRoot));
43
+ const protectedRoots = [SDK_ROOT, request.appReleaseRoot,
44
+ ...(request.dependencies?.map(function dependencyRoot(item) { return item.releaseRoot; }) ?? []),
45
+ ...(request.protectedRoots ?? [])];
46
+ for (const location of protectedRoots) {
47
+ if (!location) continue;
48
+ const root = await futureRealPath(path.resolve(location));
49
+ // SDK/release roots must never be replaced by generated output. A
50
+ // caller may select its ordinary project-local dist descendant.
51
+ if (contains(output, root) || (location !== SDK_ROOT && contains(root, output))) {
52
+ throw new ArcaneError(ERROR_CODES.usage, `Portable output overlaps selected source: ${location}.`);
53
+ }
54
+ }
55
+ await mkdir(output, {recursive: true});
56
+ return output;
57
+ }
58
+
59
+ function serviceSelection(services, release) {
60
+ return services.map(function selectService(service) {
61
+ if (!is.string(service?.module) || !release.files.includes(service.module)) {
62
+ throw new ArcaneError(ERROR_CODES.prerequisiteMissing, `The selected release does not contain service module ${String(service?.module)}.`);
63
+ }
64
+ return {module: service.module, options: service.options === undefined ? {} : service.options};
65
+ });
66
+ }
67
+
68
+ function eventOwnerPath(release) {
69
+ const paths = release.files.filter(function sdkEventModule(relative) {
70
+ return relative === 'sdk/event-manager.mjs' || relative.endsWith('/sdk/event-manager.mjs')
71
+ || /^node_modules\/(?:@[^/]+\/)?[^/]+\/browser-runtime\/event-manager\.mjs$/u.test(relative);
72
+ });
73
+ if (paths.length !== 1) {
74
+ throw new ArcaneError(ERROR_CODES.prerequisiteMissing,
75
+ 'The portable app release must select one shared SDK event-manager module.',
76
+ {details: {paths}});
77
+ }
78
+ // Native hosts mount the complete app directory at one local URL origin.
79
+ // An origin-root URL keeps nested navigable documents on the same owner.
80
+ return '/' + paths[0].split('/').map(encodeURIComponent).join('/');
81
+ }
82
+
83
+ /** Package-owned assembly; never imports an OS checkout or application service. */
84
+ export function createPortableNativeProvider({services = []} = {}) {
85
+ return {
86
+ protocol: PROTOCOL,
87
+ async describe() {
88
+ return {
89
+ protocol: PROTOCOL,
90
+ id: 'arcane-sdk-portable',
91
+ targets: ['portable'],
92
+ executable: false,
93
+ serviceConfiguration: 'explicit release module factories'
94
+ };
95
+ },
96
+ async doctor({targetRequest, signal, onEvent} = {}) {
97
+ throwIfAborted(signal);
98
+ portableTarget(targetRequest);
99
+ const missing = [];
100
+ for (const relative of CORE_FILES) {
101
+ try { await stat(path.join(SDK_ROOT, relative)); }
102
+ catch (error) {
103
+ if (error.code !== 'ENOENT') throw error;
104
+ missing.push(relative);
105
+ }
106
+ }
107
+ const result = {ready: missing.length === 0, target: 'portable', sdkRoot: SDK_ROOT, missing, executable: false};
108
+ await emit(onEvent, {type: 'native.doctor.completed', ...result});
109
+ return result;
110
+ },
111
+ async prepare({targetRequest, signal, onEvent} = {}) {
112
+ throwIfAborted(signal);
113
+ const target = portableTarget(targetRequest);
114
+ const metadata = JSON.parse(await readFile(path.join(SDK_ROOT, 'package.json'), 'utf8'));
115
+ const result = {sdkRoot: SDK_ROOT, sdkVersion: metadata.version, target, executable: false};
116
+ await emit(onEvent, {type: 'native.prepared', target: 'portable', sdkVersion: metadata.version});
117
+ return result;
118
+ },
119
+ async build(request = {}) {
120
+ const {appDescriptor, release, signal, onEvent} = request;
121
+ throwIfAborted(signal);
122
+ const target = portableTarget(request.targetRequest);
123
+ const selectedServices = serviceSelection(services, release);
124
+ const eventOwnerModuleURL = eventOwnerPath(release);
125
+ const output = await outputDirectory(request);
126
+ throwIfAborted(signal);
127
+ // Unique assembly directories preserve earlier outputs and avoid
128
+ // overwriting an app's files or a concurrent packaging operation.
129
+ const root = await mkdtemp(path.join(output, 'arcane-portable-'));
130
+ const files = [];
131
+ try {
132
+ await emit(onEvent, {type: 'native.payload.started', appId: appDescriptor.id, outputRoot: root});
133
+ await copyRelease({...request, releaseRoot: request.appReleaseRoot}, 'app', root, files, signal);
134
+ const dependencies = [];
135
+ for (const [index, dependency] of (request.dependencies ?? []).entries()) {
136
+ const directory = `dependencies/${index}`;
137
+ await copyRelease(dependency, directory, root, files, signal);
138
+ dependencies.push({appId: dependency.appId, root: directory, release: dependency.release});
139
+ }
140
+ const sdk = await copyCoreRuntime(root, files, signal);
141
+ const selectedSdk = request.selectedSdk;
142
+ let appSdk;
143
+ if (selectedSdk && release.files.some(function selectedPackageFile(relative) {
144
+ return relative.startsWith(`${selectedSdk.packageSource}/`);
145
+ })) {
146
+ const destination = `app/${selectedSdk.packageSource}`;
147
+ let metadata;
148
+ if (release.files.includes(`${selectedSdk.packageSource}/package.json`)) {
149
+ metadata = JSON.parse(await readFile(outputFile(root, `${destination}/package.json`), 'utf8'));
150
+ } else {
151
+ metadata = await copyCoreRuntime(root, files, signal, {
152
+ sourceRoot: selectedSdk.packageRoot, sdkDestination: destination
153
+ });
154
+ }
155
+ appSdk = {name: metadata.name, version: metadata.version, root: destination};
156
+ }
157
+ await writeOutput(root, 'package.json', '{"type":"module"}\n', files, signal);
158
+ await writeOutput(root, 'runtime/arcane-core.mjs', coreEntrySource(appDescriptor, sdk.version, selectedServices), files, signal);
159
+ await writeOutput(root, 'runtime/arcane-api.js', await createCoreClassicSource({eventOwnerModuleURL}), files, signal);
160
+ const manifest = {
161
+ schemaVersion: 1,
162
+ kind: 'arcane-portable-native',
163
+ sdk: {name: sdk.name, version: sdk.version},
164
+ ...(appSdk ? {appSdk} : {}),
165
+ app: appDescriptor,
166
+ target,
167
+ webRoot: 'app',
168
+ start: release.manifest?.app?.start ?? './index.html',
169
+ core: {entry: 'runtime/arcane-core.mjs', transport: 'stdio', protocol: 'arcane/1', services: selectedServices},
170
+ client: {source: 'runtime/arcane-api.js', injection: 'document-created', eventOwnerModuleURL},
171
+ dependencies,
172
+ minimumCoreVersion: request.minimumCoreVersion ?? null,
173
+ files: [...files, MANIFEST]
174
+ };
175
+ await writeOutput(root, MANIFEST, JSON.stringify(manifest, null, 2) + '\n', files, signal);
176
+ throwIfAborted(signal);
177
+ await emit(onEvent, {type: 'native.payload.completed', appId: appDescriptor.id, outputRoot: root});
178
+ throwIfAborted(signal);
179
+ return {app: appDescriptor, target: {...target, rootDir: root}, manifest};
180
+ } catch (error) {
181
+ try { await rm(root, {recursive: true, force: true}); }
182
+ catch (cleanupError) {
183
+ throw new AggregateError([error, cleanupError], `Portable assembly failed; its incomplete output remains at ${root}.`);
184
+ }
185
+ throw error;
186
+ }
187
+ },
188
+ async verify({artifact, targetRequest, signal, onEvent} = {}) {
189
+ throwIfAborted(signal);
190
+ portableTarget(targetRequest);
191
+ const root = artifact?.target?.rootDir;
192
+ if (!is.string(root) || !root) throw new ArcaneError(ERROR_CODES.usage, 'Select a portable artifact root.');
193
+ const manifest = JSON.parse(await readFile(path.join(root, MANIFEST), 'utf8'));
194
+ for (const relative of manifest.files) {
195
+ throwIfAborted(signal);
196
+ const info = await stat(outputFile(root, relative));
197
+ if (!info.isFile()) throw new ArcaneError(ERROR_CODES.operationFailed, `Portable file is unavailable: ${relative}.`);
198
+ }
199
+ const result = {target: 'portable', outputRoot: root, manifest, executable: false};
200
+ await emit(onEvent, {type: 'native.verified', target: 'portable', outputRoot: root});
201
+ return result;
202
+ },
203
+ async run() {
204
+ throw new ArcaneError(ERROR_CODES.nativeRunUnsupported,
205
+ 'Portable is a Core/app payload, not a native executable. Select an implemented platform host to launch it.');
206
+ }
207
+ };
208
+ }
209
+
210
+ export const arcaneNativeBuilderProvider = createPortableNativeProvider();
211
+ export default arcaneNativeBuilderProvider;
@@ -91,10 +91,13 @@ function selectedTargetRequest(value){
91
91
  if(value.format!==definition.format){
92
92
  fail(`Target ${value.target} requires format ${definition.format}.`,ERROR_CODES.targetUnavailable);
93
93
  }
94
- if(value.signing!==definition.signingMode){
94
+ const nestedSigning=isObject(value.signing);
95
+ const signing=nestedSigning?value.signing.mode:value.signing;
96
+ const signingProfileId=(nestedSigning?value.signing.profileId:value.signingProfileId)??null;
97
+ if(signing!==definition.signingMode){
95
98
  fail(`Target ${value.target} requires signing mode ${definition.signingMode}.`,ERROR_CODES.targetUnavailable);
96
99
  }
97
- if((value.signingProfileId??null)!==definition.signingProfileId){
100
+ if(signingProfileId!==definition.signingProfileId){
98
101
  fail(`Target ${value.target} has a different signing profile.`,ERROR_CODES.targetUnavailable);
99
102
  }
100
103
  return {
@@ -102,8 +105,8 @@ function selectedTargetRequest(value){
102
105
  platform:value.platform,
103
106
  architecture:value.architecture,
104
107
  format:value.format,
105
- signing:value.signing,
106
- signingProfileId:value.signingProfileId??null
108
+ signing,
109
+ signingProfileId
107
110
  };
108
111
  }
109
112
 
@@ -177,6 +180,7 @@ export async function createNativeBuildPlan({
177
180
  appReleaseRoot,
178
181
  release,
179
182
  appDescriptor,
183
+ selectedSdk,
180
184
  dependencyReleases,
181
185
  minimumCoreVersion,
182
186
  protectedRoots=[],
@@ -211,6 +215,7 @@ export async function createNativeBuildPlan({
211
215
  toolchainRoot:selectedToolchainRoot,
212
216
  toolchain:{...toolchain},
213
217
  appDescriptor:descriptor,
218
+ ...(selectedSdk===undefined?{}:{selectedSdk}),
214
219
  application,
215
220
  dependencies,
216
221
  minimumCoreVersion:minimumCoreVersion??null,
@@ -244,6 +249,7 @@ export async function executeNativeBuildPlan(plan,{
244
249
  toolchainRoot:plan.toolchainRoot,
245
250
  toolchain:plan.toolchain,
246
251
  appDescriptor:plan.appDescriptor,
252
+ ...(plan.selectedSdk===undefined?{}:{selectedSdk:plan.selectedSdk}),
247
253
  appReleaseRoot:plan.application.root,
248
254
  release:plan.application.release,
249
255
  readReleaseFile:plan.application.readFile,
@@ -1,7 +1,7 @@
1
1
  import Is from 'strong-type';
2
2
  import {lstat,realpath} from 'node:fs/promises';
3
3
  import path from 'node:path';
4
- import {pathToFileURL} from 'node:url';
4
+ import {fileURLToPath,pathToFileURL} from 'node:url';
5
5
  import {ArcaneError,ERROR_CODES,throwIfAborted} from './errors.mjs';
6
6
  import {validateNativeBuilder} from './native-plan.mjs';
7
7
 
@@ -106,7 +106,11 @@ async function resolveProviderLocation({
106
106
  providerPath:canonicalProvider
107
107
  });
108
108
  }
109
- return {canonicalRoot,providerPath:canonicalProvider};
109
+ return {
110
+ canonicalRoot,
111
+ providerPath:canonicalProvider,
112
+ providerSource:'arcane-checkout'
113
+ };
110
114
  }
111
115
 
112
116
  export async function loadArcaneNativeProvider(options={}){
@@ -129,7 +133,13 @@ export async function loadArcaneNativeProvider(options={}){
129
133
  target,
130
134
  message:`Loading the Arcane ${String(target)} provider.`
131
135
  });
132
- const location=await resolveProviderLocation({arcaneRoot,target,inspect,canonicalize});
136
+ const location=arcaneRoot===undefined&&target==='portable'
137
+ ?{
138
+ canonicalRoot:path.resolve(fileURLToPath(new URL('../',import.meta.url))),
139
+ providerPath:fileURLToPath(new URL('./native/portable-provider.mjs',import.meta.url)),
140
+ providerSource:'sdk-package'
141
+ }
142
+ :await resolveProviderLocation({arcaneRoot,target,inspect,canonicalize});
133
143
  throwIfAborted(signal);
134
144
  let namespace;
135
145
  try{
@@ -147,9 +157,10 @@ export async function loadArcaneNativeProvider(options={}){
147
157
  );
148
158
  throwIfAborted(signal);
149
159
  const pairing={
150
- arcaneRoot:location.canonicalRoot,
160
+ arcaneRoot:location.providerSource==='arcane-checkout'?location.canonicalRoot:null,
151
161
  toolchainRoot:location.canonicalRoot,
152
162
  providerPath:location.providerPath,
163
+ providerSource:location.providerSource,
153
164
  nativeBuilder
154
165
  };
155
166
  await onEvent?.({