arcane-os 0.5.17 → 0.5.18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +24 -0
- package/README.md +9 -9
- package/bin/arcane-test.mjs +16 -13
- package/browser-runtime/ai/browser-speech-artifacts.mjs +41 -38
- package/browser-runtime/ai/browser-speech-providers.mjs +46 -43
- package/browser-runtime/ai/browser-wasm-llm-provider.mjs +127 -124
- package/browser-runtime/ai/browser-wasm.mjs +6 -3
- package/browser-runtime/ai/browser-wllama-runtime.mjs +26 -23
- package/browser-runtime/ai/model-controller.mjs +94 -91
- package/browser-runtime/ai/speech-worker-client.mjs +12 -9
- package/browser-runtime/ai/speech-worker-runtime.mjs +56 -53
- package/browser-runtime/dependencies/strong-type/index.js +207 -82
- package/browser-runtime/dependencies/strong-type/package.json +15 -6
- package/browser-runtime/dom-event-instrumentation.mjs +24 -20
- package/browser-runtime/event-manager.mjs +111 -108
- package/browser-runtime/speech-text.mjs +5 -1
- package/docs/reference/ai/browser-speech.md +198 -5
- package/docs/reference/availability-and-normalization.md +40 -0
- package/docs/reference/inventory/package-api.json +1 -1
- package/docs/reference/inventory/runtime-components.json +1 -1
- package/docs/reference/inventory/runtime-modules.json +20 -24
- package/docs/reference/protocols.md +1 -1
- package/docs/reference/runtime-modules.md +125 -35
- package/docs/reference/sdk-api.md +23 -7
- package/examples/wasm-ai-demo/index.html +1 -0
- package/node_modules/event-pubsub/node_modules/strong-type/README.md +408 -0
- package/node_modules/event-pubsub/node_modules/strong-type/assets/strong-type-header.png +0 -0
- package/node_modules/event-pubsub/node_modules/strong-type/index.js +1151 -0
- package/node_modules/event-pubsub/node_modules/strong-type/licence +21 -0
- package/node_modules/event-pubsub/node_modules/strong-type/node.js +125 -0
- package/node_modules/event-pubsub/node_modules/strong-type/package.json +61 -0
- package/node_modules/strong-type/README.md +42 -24
- package/node_modules/strong-type/benchmark/core.js +324 -0
- package/node_modules/strong-type/index.js +207 -82
- package/node_modules/strong-type/package.json +14 -6
- package/package.json +3 -3
- package/runtime/arcane/components/app-bar.html +5 -2
- package/runtime/arcane/components/assistant-panel.html +5 -2
- package/runtime/arcane/components/chart.html +15 -12
- package/runtime/arcane/components/chat.html +117 -138
- package/runtime/arcane/components/dashboard-config.html +6 -3
- package/runtime/arcane/components/data-view.html +4 -1
- package/runtime/arcane/components/directory-picker.html +5 -2
- package/runtime/arcane/components/document-inspector.html +6 -3
- package/runtime/arcane/components/file-drop.html +5 -2
- package/runtime/arcane/components/file-inspector.html +9 -6
- package/runtime/arcane/components/file-manager.html +16 -13
- package/runtime/arcane/components/local-ai-status.html +5 -2
- package/runtime/arcane/components/markdown-document.html +14 -11
- package/runtime/arcane/components/markdown-editor.html +4 -1
- package/runtime/arcane/components/modal.html +4 -1
- package/runtime/arcane/components/output-panel.html +6 -3
- package/runtime/arcane/components/preferences-form.html +4 -1
- package/runtime/arcane/components/record-timeline.html +8 -5
- package/runtime/arcane/components/relationship-board.html +9 -6
- package/runtime/arcane/components/source-code-viewer.html +12 -9
- package/runtime/arcane/components/source-explanation.html +8 -5
- package/runtime/arcane/components/speech.html +24 -21
- package/runtime/arcane/components/summary-strip.html +4 -1
- package/runtime/arcane/components/task-progress.html +6 -3
- package/runtime/arcane/components/terminal-workspace.html +4 -1
- package/runtime/arcane/components/voice-transcription.html +11 -8
- package/runtime/arcane/components/web-navigator.html +5 -2
- package/runtime/arcane/entities/ApiModelRecord.js +5 -2
- package/runtime/arcane/entities/Calculation.js +5 -2
- package/runtime/arcane/entities/Chat.js +33 -33
- package/runtime/arcane/entities/CommunicationMessage.js +4 -1
- package/runtime/arcane/entities/CommunicationThread.js +4 -1
- package/runtime/arcane/entities/File.js +1 -1
- package/runtime/arcane/entities/Image.js +8 -5
- package/runtime/arcane/entities/IntentEnvelope.js +20 -17
- package/runtime/arcane/entities/Preference.js +9 -6
- package/runtime/arcane/entities/Theme.js +5 -2
- package/runtime/arcane/entities/User.js +11 -11
- package/runtime/arcane/entities/Weather.js +5 -2
- package/runtime/arcane/modules/AI.js +491 -165
- package/runtime/arcane/modules/AIPreferenceRuntime.js +5 -2
- package/runtime/arcane/modules/AIPreferenceTuple.js +9 -6
- package/runtime/arcane/modules/AIProviderRuntime.js +88 -85
- package/runtime/arcane/modules/AIResponseURLPolicy.js +148 -62
- package/runtime/arcane/modules/AIRuntimeState.js +29 -26
- package/runtime/arcane/modules/AnsiText.js +4 -1
- package/runtime/arcane/modules/ApiModelDatabase.js +22 -19
- package/runtime/arcane/modules/AppDataScope.js +8 -5
- package/runtime/arcane/modules/ArcaneCommunicationBridge.js +4 -1
- package/runtime/arcane/modules/ArcaneNavigationPolicy.js +8 -5
- package/runtime/arcane/modules/ArcaneNetworkPolicy.js +18 -15
- package/runtime/arcane/modules/AsyncBoundary.js +13 -10
- package/runtime/arcane/modules/BrowserTestSuite.js +19 -16
- package/runtime/arcane/modules/CalculatorEngine.js +5 -2
- package/runtime/arcane/modules/ChatRecords.js +18 -15
- package/runtime/arcane/modules/CommunicationAppController.js +9 -6
- package/runtime/arcane/modules/CommunicationHub.js +9 -6
- package/runtime/arcane/modules/CommunicationPreferences.js +4 -1
- package/runtime/arcane/modules/CommunicationProviderRegistry.js +4 -1
- package/runtime/arcane/modules/ComponentContracts.js +56 -53
- package/runtime/arcane/modules/ConfiguredAIChatSession.js +30 -27
- package/runtime/arcane/modules/ConversationActionItems.js +15 -12
- package/runtime/arcane/modules/ConversationClosingReport.js +11 -8
- package/runtime/arcane/modules/ConversationTimebox.js +28 -25
- package/runtime/arcane/modules/CoreLocalModelCatalog.js +17 -14
- package/runtime/arcane/modules/DBLS.js +7 -4
- package/runtime/arcane/modules/DBOPFS.js +7 -7
- package/runtime/arcane/modules/DBOPFSDocumentLibrary.js +34 -31
- package/runtime/arcane/modules/DevelopmentWorkspace.js +4 -1
- package/runtime/arcane/modules/DirectoryPicker.js +8 -5
- package/runtime/arcane/modules/DocumentLexicalSearch.js +11 -8
- package/runtime/arcane/modules/DocumentNavigation.js +7 -4
- package/runtime/arcane/modules/Errors.js +28 -25
- package/runtime/arcane/modules/HTMLImport.js +7 -4
- package/runtime/arcane/modules/IsolatedModelQuestionRunner.js +15 -12
- package/runtime/arcane/modules/LocalAIReadiness.js +21 -18
- package/runtime/arcane/modules/LocalAIReadinessController.js +6 -3
- package/runtime/arcane/modules/MD.js +1 -1
- package/runtime/arcane/modules/Mail.js +46 -43
- package/runtime/arcane/modules/MailOutbox.mjs +48 -45
- package/runtime/arcane/modules/MailTransport.mjs +29 -26
- package/runtime/arcane/modules/MemoryRecords.js +7 -4
- package/runtime/arcane/modules/MessageAdvisory.js +6 -3
- package/runtime/arcane/modules/ModelDefinition.js +7 -4
- package/runtime/arcane/modules/Ollama.js +6 -3
- package/runtime/arcane/modules/OllamaModelIdentifier.js +4 -1
- package/runtime/arcane/modules/OpenMeteoWeatherProvider.js +18 -15
- package/runtime/arcane/modules/PersistentAIChatSession.js +33 -30
- package/runtime/arcane/modules/PreferenceStore.js +20 -17
- package/runtime/arcane/modules/PreparedSpeech.js +485 -0
- package/runtime/arcane/modules/Questionnaire.js +6 -3
- package/runtime/arcane/modules/RecordLinkIndex.js +4 -1
- package/runtime/arcane/modules/RecordPassageIndex.js +9 -6
- package/runtime/arcane/modules/RecordReviewStore.js +12 -9
- package/runtime/arcane/modules/RiskSignalAnalyzer.js +4 -1
- package/runtime/arcane/modules/ScopedOPFSCache.js +6 -3
- package/runtime/arcane/modules/ScreenCapture.js +20 -17
- package/runtime/arcane/modules/SpeechPlayback.js +29 -26
- package/runtime/arcane/modules/StaticDocumentCatalog.js +33 -30
- package/runtime/arcane/modules/SystemAppearance.js +5 -2
- package/runtime/arcane/modules/SystemToolRegistry.js +6 -3
- package/runtime/arcane/modules/TerminalClient.js +14 -11
- package/runtime/arcane/modules/TerminalCommandRegistry.js +4 -1
- package/runtime/arcane/modules/ThemeBootstrap.js +9 -6
- package/runtime/arcane/modules/ThemeManager.js +5 -2
- package/runtime/arcane/modules/TimeGuard.js +1 -1
- package/runtime/arcane/modules/ToolCallRouter.js +11 -8
- package/runtime/arcane/modules/WaitForComponent.js +19 -16
- package/runtime/arcane/modules/YouTubeMedia.js +4 -1
- package/runtime/strong-type/index.js +1276 -352
- package/runtime/strong-type/package.json +69 -45
- package/src/app-descriptor.mjs +17 -14
- package/src/application-tests.mjs +4 -1
- package/src/cli/main.mjs +8 -5
- package/src/constants.mjs +4 -1
- package/src/dev-server.mjs +8 -5
- package/src/doctor.mjs +5 -2
- package/src/dom-event-instrumentation.mjs +24 -20
- package/src/errors.mjs +7 -3
- package/src/event-manager.mjs +111 -108
- package/src/event-queue.mjs +8 -5
- package/src/events.mjs +12 -9
- package/src/import-map.mjs +35 -27
- package/src/installed-sdk-runtime.mjs +4 -1
- package/src/integrated-provider-loader.mjs +9 -6
- package/src/mail-credentials.mjs +16 -13
- package/src/mail-server.mjs +53 -50
- package/src/mail.mjs +18 -15
- package/src/native-plan.mjs +12 -9
- package/src/native-provider-loader.mjs +7 -4
- package/src/packager/core.mjs +18 -15
- package/src/process.mjs +6 -3
- package/src/release-bundle.mjs +14 -11
- package/src/runtime.mjs +4 -1
- package/src/scaffold.mjs +9 -6
- package/src/sdk-browser-runtime.mjs +4 -1
- package/src/source-server.mjs +17 -14
- package/src/targets/index.mjs +5 -2
- package/src/templates/workspace-template.mjs +13 -6
- package/src/testing-loader.mjs +7 -4
- package/src/testing.mjs +10 -7
- package/src/toolchain.mjs +13 -10
- package/src/update-check.mjs +9 -6
- package/src/workspace-operation-lock.mjs +14 -11
- package/src/workspace-runtime.mjs +4 -1
- package/src/workspace.mjs +17 -14
- package/runtime/arcane/modules/AIResponseLength.js +0 -32
package/src/packager/core.mjs
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import Is from 'strong-type';
|
|
1
2
|
import {
|
|
2
3
|
copyFile,
|
|
3
4
|
lstat,
|
|
@@ -14,6 +15,8 @@ import {pathToFileURL} from 'node:url';
|
|
|
14
15
|
import {withWorkspaceOperationLock} from '../workspace-operation-lock.mjs';
|
|
15
16
|
import {inspectImportMapHtml,readWorkspaceAssetVersion,rewriteAssetReferences} from '../import-map.mjs';
|
|
16
17
|
|
|
18
|
+
const is = new Is(false);
|
|
19
|
+
|
|
17
20
|
export const ROOT_CONFIG_NAME='arcane-packager.json';
|
|
18
21
|
export const APP_CONFIG_NAME='arcane-package.json';
|
|
19
22
|
export const RELEASE_MANIFEST_NAME='ARCANE_APP_RELEASE.json';
|
|
@@ -42,7 +45,7 @@ function throwIfAborted(signal){
|
|
|
42
45
|
}
|
|
43
46
|
|
|
44
47
|
async function emit(onEvent,event){
|
|
45
|
-
if(
|
|
48
|
+
if(is.function(onEvent))await onEvent(event);
|
|
46
49
|
}
|
|
47
50
|
|
|
48
51
|
function compareText(left,right){
|
|
@@ -52,7 +55,7 @@ function compareText(left,right){
|
|
|
52
55
|
}
|
|
53
56
|
|
|
54
57
|
function isPlainObject(value){
|
|
55
|
-
return value!==null&&
|
|
58
|
+
return value!==null&&is.object(value)&&!is.array(value);
|
|
56
59
|
}
|
|
57
60
|
|
|
58
61
|
function copyJson(value){
|
|
@@ -67,14 +70,14 @@ function assertOnlyKeys(value,allowed,label){
|
|
|
67
70
|
}
|
|
68
71
|
|
|
69
72
|
function normalizeWorkspaceRoot(workspaceRoot){
|
|
70
|
-
if(
|
|
73
|
+
if(!is.string(workspaceRoot)||!workspaceRoot.trim()){
|
|
71
74
|
fail('workspaceRoot must be a directory path.');
|
|
72
75
|
}
|
|
73
76
|
return path.resolve(workspaceRoot);
|
|
74
77
|
}
|
|
75
78
|
|
|
76
79
|
export function normalizeRelativePath(value,label='path'){
|
|
77
|
-
if(
|
|
80
|
+
if(!is.string(value)||!value||value.includes('\\')||TEXT_CONTROL_PATTERN.test(value)){
|
|
78
81
|
fail(`Unsafe ${label}: ${String(value)}`);
|
|
79
82
|
}
|
|
80
83
|
if(path.posix.isAbsolute(value)||/^[a-z]:/iu.test(value))fail(`Unsafe ${label}: ${value}`);
|
|
@@ -119,7 +122,7 @@ function isGlobLike(value){
|
|
|
119
122
|
}
|
|
120
123
|
|
|
121
124
|
function validatePathList(value,label,{required=false}={}){
|
|
122
|
-
if(!
|
|
125
|
+
if(!is.array(value)||(required&&value.length===0)){
|
|
123
126
|
fail(`${label} must be ${required?'a non-empty':'an'} array of literal relative paths.`);
|
|
124
127
|
}
|
|
125
128
|
const normalized=value.map((entry,index)=>{
|
|
@@ -160,14 +163,14 @@ function isExcluded(relative,excludes){
|
|
|
160
163
|
}
|
|
161
164
|
|
|
162
165
|
function assertPresentationText(value,label){
|
|
163
|
-
if(
|
|
166
|
+
if(!is.string(value)||!value.trim()){
|
|
164
167
|
fail(`${label} must be nonempty text.`);
|
|
165
168
|
}
|
|
166
169
|
return value;
|
|
167
170
|
}
|
|
168
171
|
|
|
169
172
|
export function parseSemver(value){
|
|
170
|
-
if(
|
|
173
|
+
if(!is.string(value))fail(`Invalid semantic version: ${String(value)}`);
|
|
171
174
|
const match=/^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?(?:\+([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?$/u.exec(value);
|
|
172
175
|
if(!match)fail(`Invalid semantic version: ${value}`);
|
|
173
176
|
const prerelease=match[4]?match[4].split('.'):[];
|
|
@@ -177,7 +180,7 @@ export function parseSemver(value){
|
|
|
177
180
|
}
|
|
178
181
|
}
|
|
179
182
|
const numbers=match.slice(1,4).map(Number);
|
|
180
|
-
if(numbers.some(number=>!
|
|
183
|
+
if(numbers.some(number=>!is.safeInteger(number))){
|
|
181
184
|
fail(`Semantic version component exceeds JavaScript's safe integer range: ${value}`);
|
|
182
185
|
}
|
|
183
186
|
return {
|
|
@@ -204,7 +207,7 @@ export function incrementSemver(value,bump,preid='rc'){
|
|
|
204
207
|
if(bump==='major')return formatSemver({major:current.major+1,minor:0,patch:0});
|
|
205
208
|
if(bump==='minor')return formatSemver({major:current.major,minor:current.minor+1,patch:0});
|
|
206
209
|
if(bump==='patch')return formatSemver({major:current.major,minor:current.minor,patch:current.patch+1});
|
|
207
|
-
if(
|
|
210
|
+
if(!is.string(preid)||!/^[0-9A-Za-z-]+$/u.test(preid)){
|
|
208
211
|
fail(`Invalid prerelease identifier: ${String(preid)}`);
|
|
209
212
|
}
|
|
210
213
|
const next={major:current.major,minor:current.minor,patch:current.patch,prerelease:[]};
|
|
@@ -264,7 +267,7 @@ export function validateRootConfig(value,configPath=ROOT_CONFIG_NAME){
|
|
|
264
267
|
const sharedPayloads={};
|
|
265
268
|
for(const [id,routes] of Object.entries(value.sharedPayloads).sort(([left],[right])=>compareText(left,right))){
|
|
266
269
|
if(!SAFE_SHARED_ID_PATTERN.test(id))fail(`Unsafe shared payload id: ${id}`);
|
|
267
|
-
if(!
|
|
270
|
+
if(!is.array(routes)||routes.length===0){
|
|
268
271
|
fail(`sharedPayloads.${id} must be a non-empty array.`);
|
|
269
272
|
}
|
|
270
273
|
sharedPayloads[id]=routes.map((route,index)=>
|
|
@@ -304,11 +307,11 @@ export function validateAppConfig(value,appId,rootConfig,configPath=`apps/${appI
|
|
|
304
307
|
if(!['static','adapter'].includes(value.strategy)){
|
|
305
308
|
fail(`${appId}/${APP_CONFIG_NAME}.strategy must be "static" or "adapter".`);
|
|
306
309
|
}
|
|
307
|
-
if(!
|
|
310
|
+
if(!is.array(value.shared)||new Set(value.shared).size!==value.shared.length){
|
|
308
311
|
fail(`${appId}/${APP_CONFIG_NAME}.shared must be an array of unique shared payload ids.`);
|
|
309
312
|
}
|
|
310
313
|
for(const [index,id] of value.shared.entries()){
|
|
311
|
-
if(
|
|
314
|
+
if(!is.string(id)||!Object.hasOwn(rootConfig.sharedPayloads,id)){
|
|
312
315
|
fail(`${appId}/${APP_CONFIG_NAME}.shared[${index}] references an unknown shared payload.`);
|
|
313
316
|
}
|
|
314
317
|
}
|
|
@@ -383,7 +386,7 @@ async function loadContext(requestedWorkspaceRoot,appId){
|
|
|
383
386
|
const workspaceRoot=await realDirectory(normalizeWorkspaceRoot(requestedWorkspaceRoot),'Workspace root');
|
|
384
387
|
const rootConfigPath=path.join(workspaceRoot,ROOT_CONFIG_NAME);
|
|
385
388
|
const rootConfig=validateRootConfig(await readJson(rootConfigPath,ROOT_CONFIG_NAME),rootConfigPath);
|
|
386
|
-
if(
|
|
389
|
+
if(!is.string(appId)||!APP_ID_PATTERN.test(appId))fail(`Unsafe app id: ${String(appId)}`);
|
|
387
390
|
const appsRoot=await realDirectory(path.join(workspaceRoot,rootConfig.appsRoot),'Apps root');
|
|
388
391
|
const appRoot=resolveInside(appsRoot,appId,'app id');
|
|
389
392
|
await assertContainedRealPath(appsRoot,appRoot,`apps/${appId}`);
|
|
@@ -616,7 +619,7 @@ async function loadAdapter(context){
|
|
|
616
619
|
const adapterPath=resolveInside(context.appRoot,context.config.adapter,`${context.appId} adapter`);
|
|
617
620
|
await assertContainedRealPath(context.appRoot,adapterPath,`${context.appId} adapter`);
|
|
618
621
|
const module=await import(`${pathToFileURL(adapterPath).href}?source=${Date.now()}`);
|
|
619
|
-
if(
|
|
622
|
+
if(!is.function(module.buildArcanePackage)){
|
|
620
623
|
fail(`${context.appId} adapter must export buildArcanePackage.`);
|
|
621
624
|
}
|
|
622
625
|
return module;
|
|
@@ -816,7 +819,7 @@ export async function verifyApp({workspaceRoot,appId,signal,onEvent}={}){
|
|
|
816
819
|
const manifest=await readJson(path.join(outputRoot,RELEASE_MANIFEST_NAME),RELEASE_MANIFEST_NAME);
|
|
817
820
|
if(!isPlainObject(manifest)||manifest.schemaVersion!==1||manifest.kind!=='arcane-app-release'
|
|
818
821
|
||manifest.packagerVersion!==PACKAGER_VERSION||manifest.app?.id!==appId
|
|
819
|
-
||manifest.app?.version!==context.config.version||!
|
|
822
|
+
||manifest.app?.version!==context.config.version||!is.array(manifest.files)){
|
|
820
823
|
fail(`${RELEASE_MANIFEST_NAME} is malformed.`);
|
|
821
824
|
}
|
|
822
825
|
const expected=manifest.files.map((file,index)=>normalizeRelativePath(
|
package/src/process.mjs
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
|
+
import Is from 'strong-type';
|
|
1
2
|
import {spawn} from 'node:child_process';
|
|
2
3
|
import path from 'node:path';
|
|
3
4
|
import {ArcaneError,ERROR_CODES,throwIfAborted} from './errors.mjs';
|
|
4
5
|
import {createEventQueue} from './event-queue.mjs';
|
|
5
6
|
|
|
7
|
+
const is = new Is(false);
|
|
8
|
+
|
|
6
9
|
const DEFAULT_TERMINATION_GRACE_MS=1500;
|
|
7
10
|
|
|
8
11
|
export function platformCommand(command,platform=process.platform){
|
|
@@ -58,7 +61,7 @@ function childIsRunning(child){
|
|
|
58
61
|
}
|
|
59
62
|
|
|
60
63
|
function childHasPid(child){
|
|
61
|
-
return
|
|
64
|
+
return is.integer(child?.pid)&&child.pid>0;
|
|
62
65
|
}
|
|
63
66
|
|
|
64
67
|
function terminateWindowsTree(child,{force=false}={}){
|
|
@@ -124,10 +127,10 @@ export async function runProcess(command,args=[],{
|
|
|
124
127
|
input
|
|
125
128
|
}={}){
|
|
126
129
|
throwIfAborted(signal);
|
|
127
|
-
if(!
|
|
130
|
+
if(!is.array(args)||args.some(argument=>!is.string(argument))){
|
|
128
131
|
throw new ArcaneError(ERROR_CODES.usage,'Process arguments must be a fixed array of strings.');
|
|
129
132
|
}
|
|
130
|
-
if(!
|
|
133
|
+
if(!is.integer(terminationGraceMs)||terminationGraceMs<100||terminationGraceMs>30_000){
|
|
131
134
|
throw new ArcaneError(
|
|
132
135
|
ERROR_CODES.usage,
|
|
133
136
|
'terminationGraceMs must be an integer from 100 through 30000.'
|
package/src/release-bundle.mjs
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import Is from 'strong-type';
|
|
1
2
|
import {
|
|
2
3
|
lstat,
|
|
3
4
|
mkdir,
|
|
@@ -15,6 +16,8 @@ import {SDK_NAME,SDK_VERSION} from './constants.mjs';
|
|
|
15
16
|
import {ArcaneError,ERROR_CODES,throwIfAborted} from './errors.mjs';
|
|
16
17
|
import {RELEASE_MANIFEST_NAME,PACKAGER_VERSION,parseSemver} from './packager/core.mjs';
|
|
17
18
|
|
|
19
|
+
const is = new Is(false);
|
|
20
|
+
|
|
18
21
|
export const APP_BUNDLE_MANIFEST_NAME='ARCANE_APP_BUNDLE.json';
|
|
19
22
|
export const APP_BUNDLE_DESCRIPTOR_NAME='arcane-app.json';
|
|
20
23
|
export const APP_BUNDLE_RELEASE_PATH=`payload/${RELEASE_MANIFEST_NAME}`;
|
|
@@ -41,7 +44,7 @@ function fail(message,code='ARCANE_BUNDLE_INVALID',details){
|
|
|
41
44
|
}
|
|
42
45
|
|
|
43
46
|
async function emit(onEvent,event){
|
|
44
|
-
if(
|
|
47
|
+
if(is.function(onEvent))await onEvent(event);
|
|
45
48
|
}
|
|
46
49
|
|
|
47
50
|
function compareText(left,right){
|
|
@@ -51,7 +54,7 @@ function compareText(left,right){
|
|
|
51
54
|
}
|
|
52
55
|
|
|
53
56
|
function isPlainObject(value){
|
|
54
|
-
return value!==null&&
|
|
57
|
+
return value!==null&&is.object(value)&&!is.array(value);
|
|
55
58
|
}
|
|
56
59
|
|
|
57
60
|
function copyJson(value){
|
|
@@ -63,7 +66,7 @@ function pathKey(value){
|
|
|
63
66
|
}
|
|
64
67
|
|
|
65
68
|
export function validateAppBundlePath(value,label='bundle path'){
|
|
66
|
-
if(
|
|
69
|
+
if(!is.string(value)||!value||value!==value.normalize('NFC')
|
|
67
70
|
||value.includes('\\')||value.startsWith('/')||/^[a-z]:/iu.test(value)
|
|
68
71
|
||/[\u0000-\u001f\u007f]/u.test(value)){
|
|
69
72
|
fail(`Unsafe ${label}: ${String(value)}.`);
|
|
@@ -100,7 +103,7 @@ function writeTextField(header,offset,length,value,label){
|
|
|
100
103
|
}
|
|
101
104
|
|
|
102
105
|
function writeOctalField(header,offset,length,value,label,{trailingSpace=false}={}){
|
|
103
|
-
if(!
|
|
106
|
+
if(!is.safeInteger(value)||value<0)fail(`${label} must be a nonnegative integer.`);
|
|
104
107
|
const terminal=trailingSpace?'\0 ':'\0';
|
|
105
108
|
const digits=value.toString(8);
|
|
106
109
|
const available=length-terminal.length;
|
|
@@ -184,8 +187,8 @@ function readJsonContent(content,label){
|
|
|
184
187
|
function validateReleaseManifest(value){
|
|
185
188
|
if(!isPlainObject(value)||value.schemaVersion!==1||value.kind!=='arcane-app-release'
|
|
186
189
|
||value.packagerVersion!==PACKAGER_VERSION||!isPlainObject(value.app)
|
|
187
|
-
|
|
188
|
-
||!
|
|
190
|
+
||!is.string(value.app.id)||!is.string(value.app.version)
|
|
191
|
+
||!is.array(value.files)){
|
|
189
192
|
fail(`${RELEASE_MANIFEST_NAME} is malformed.`);
|
|
190
193
|
}
|
|
191
194
|
parseSemver(value.app.version);
|
|
@@ -213,8 +216,8 @@ function bundleManifest(descriptor,release,files){
|
|
|
213
216
|
}
|
|
214
217
|
|
|
215
218
|
async function outputBoundary(outputPath,overwrite){
|
|
216
|
-
if(
|
|
217
|
-
if(
|
|
219
|
+
if(!is.string(outputPath)||!outputPath.trim())fail('outputPath is required.');
|
|
220
|
+
if(!is.boolean(overwrite))fail('overwrite must be a boolean.',ERROR_CODES.usage);
|
|
218
221
|
const resolved=path.resolve(outputPath);
|
|
219
222
|
await mkdir(path.dirname(resolved),{recursive:true});
|
|
220
223
|
const parent=await realDirectory(path.dirname(resolved),'Bundle output directory');
|
|
@@ -301,7 +304,7 @@ function readOctalField(header,offset,length,label){
|
|
|
301
304
|
const text=header.subarray(offset,offset+length).toString('ascii').replace(/[\0 ]+$/u,'');
|
|
302
305
|
if(!/^[0-7]+$/u.test(text))fail(`${label} is not a valid ustar octal field.`);
|
|
303
306
|
const value=Number.parseInt(text,8);
|
|
304
|
-
if(!
|
|
307
|
+
if(!is.safeInteger(value))fail(`${label} exceeds the supported integer range.`);
|
|
305
308
|
return value;
|
|
306
309
|
}
|
|
307
310
|
|
|
@@ -344,7 +347,7 @@ function requiredEntry(entries,entryPath){
|
|
|
344
347
|
|
|
345
348
|
export async function verifyAppReleaseBundle({bundlePath,signal,onEvent}={}){
|
|
346
349
|
throwIfAborted(signal);
|
|
347
|
-
if(
|
|
350
|
+
if(!is.string(bundlePath)||!bundlePath.trim())fail('bundlePath is required.');
|
|
348
351
|
const selected=path.resolve(bundlePath);
|
|
349
352
|
const info=await lstat(selected);
|
|
350
353
|
if(info.isSymbolicLink()||!info.isFile())fail('Bundle path must be a real file.');
|
|
@@ -360,7 +363,7 @@ export async function verifyAppReleaseBundle({bundlePath,signal,onEvent}={}){
|
|
|
360
363
|
||manifest.kind!==APP_BUNDLE_KIND||manifest.format!==APP_BUNDLE_FORMAT
|
|
361
364
|
||manifest.sdk?.name!==SDK_NAME||manifest.sdk?.version!==SDK_VERSION
|
|
362
365
|
||manifest.descriptor!==APP_BUNDLE_DESCRIPTOR_NAME
|
|
363
|
-
||manifest.release!==APP_BUNDLE_RELEASE_PATH||!
|
|
366
|
+
||manifest.release!==APP_BUNDLE_RELEASE_PATH||!is.array(manifest.files)){
|
|
364
367
|
fail(`${APP_BUNDLE_MANIFEST_NAME} is malformed.`);
|
|
365
368
|
}
|
|
366
369
|
const descriptor=copyJson(validateAppDescriptor(readJsonContent(
|
package/src/runtime.mjs
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
|
+
import Is from 'strong-type';
|
|
1
2
|
import {lstat,readFile,readdir,realpath} from 'node:fs/promises';
|
|
2
3
|
import path from 'node:path';
|
|
3
4
|
import {fileURLToPath} from 'node:url';
|
|
4
5
|
import {SDK_VERSION} from './constants.mjs';
|
|
5
6
|
|
|
7
|
+
const is = new Is(false);
|
|
8
|
+
|
|
6
9
|
const sdkRoot=path.resolve(path.dirname(fileURLToPath(import.meta.url)),'..');
|
|
7
10
|
|
|
8
11
|
function fail(message,code='ARCANE_RUNTIME_INVALID'){
|
|
@@ -25,7 +28,7 @@ function compareText(left,right){
|
|
|
25
28
|
}
|
|
26
29
|
|
|
27
30
|
function safeRelativePath(value){
|
|
28
|
-
if(
|
|
31
|
+
if(!is.string(value)||!value||value.includes('\\')||value.includes('\0')
|
|
29
32
|
||path.posix.isAbsolute(value)||path.posix.normalize(value)!==value
|
|
30
33
|
||value==='.'||value.startsWith('../')||value.includes('/../')){
|
|
31
34
|
fail(`SDK runtime contains an unsafe relative path: ${String(value)}.`);
|
package/src/scaffold.mjs
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import Is from 'strong-type';
|
|
1
2
|
import {lstat,mkdir,readFile,readdir,writeFile} from 'node:fs/promises';
|
|
2
3
|
import path from 'node:path';
|
|
3
4
|
import {spawn} from 'node:child_process';
|
|
@@ -8,6 +9,8 @@ import {SDK_NAME,SDK_VERSION,workspaceTemplate} from './templates/workspace-temp
|
|
|
8
9
|
import {inspectWorkspaceProfile,resolveSdkPackageDeclaration} from './workspace.mjs';
|
|
9
10
|
import {parseSemver} from './packager/core.mjs';
|
|
10
11
|
|
|
12
|
+
const is = new Is(false);
|
|
13
|
+
|
|
11
14
|
const APP_ID_PATTERN=/^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/;
|
|
12
15
|
const DISPLAY_CONTROL_PATTERN=/[\x00-\x1f\x7f]/;
|
|
13
16
|
|
|
@@ -25,14 +28,14 @@ function throwIfAborted(signal){
|
|
|
25
28
|
}
|
|
26
29
|
|
|
27
30
|
async function emit(onEvent,event){
|
|
28
|
-
if(
|
|
31
|
+
if(is.function(onEvent))await onEvent(event);
|
|
29
32
|
}
|
|
30
33
|
|
|
31
34
|
function validateInputs(appId,displayName){
|
|
32
|
-
if(
|
|
35
|
+
if(!is.string(appId)||!APP_ID_PATTERN.test(appId)){
|
|
33
36
|
fail(`Invalid app id: ${String(appId)}. Use lowercase words separated by hyphens.`,'ARCANE_USAGE');
|
|
34
37
|
}
|
|
35
|
-
if(displayName!==undefined&&(
|
|
38
|
+
if(displayName!==undefined&&(!is.string(displayName)||displayName!==displayName.trim()
|
|
36
39
|
||!displayName||displayName.length>160||DISPLAY_CONTROL_PATTERN.test(displayName)
|
|
37
40
|
||/[<>]/.test(displayName))){
|
|
38
41
|
fail('displayName must be plain trimmed text no longer than 160 characters.','ARCANE_USAGE');
|
|
@@ -162,7 +165,7 @@ async function readExistingPackage(workspaceRoot){
|
|
|
162
165
|
let existing;
|
|
163
166
|
try{existing=JSON.parse(source);}
|
|
164
167
|
catch(error){fail(`Existing package.json is not valid JSON: ${error.message}.`);}
|
|
165
|
-
if(!existing
|
|
168
|
+
if(!existing||!is.object(existing)||is.array(existing))fail('Existing package.json must be a JSON object.');
|
|
166
169
|
return {exists:true,packagePath,source,document:existing};
|
|
167
170
|
}
|
|
168
171
|
|
|
@@ -315,8 +318,8 @@ export async function createWorkspace({
|
|
|
315
318
|
}){
|
|
316
319
|
validateInputs(appId,displayName);
|
|
317
320
|
validateScaffoldTarget(target);
|
|
318
|
-
if(
|
|
319
|
-
if(
|
|
321
|
+
if(!is.string(targetPath)||!targetPath.trim())fail('targetPath is required.','ARCANE_USAGE');
|
|
322
|
+
if(!is.boolean(initializeGit))fail('initializeGit must be a boolean.','ARCANE_USAGE');
|
|
320
323
|
throwIfAborted(signal);
|
|
321
324
|
const workspaceRoot=path.resolve(targetPath);
|
|
322
325
|
await emit(onEvent,{type:'scaffold.started',mode:'create',workspaceRoot,appId,target});
|
|
@@ -1,8 +1,11 @@
|
|
|
1
|
+
import Is from 'strong-type';
|
|
1
2
|
import {lstat,readFile,readdir,realpath} from 'node:fs/promises';
|
|
2
3
|
import path from 'node:path';
|
|
3
4
|
import {fileURLToPath} from 'node:url';
|
|
4
5
|
import {SDK_VERSION} from './constants.mjs';
|
|
5
6
|
|
|
7
|
+
const is = new Is(false);
|
|
8
|
+
|
|
6
9
|
const packageRoot=path.resolve(path.dirname(fileURLToPath(import.meta.url)),'..');
|
|
7
10
|
const defaultRoot=path.join(packageRoot,'browser-runtime');
|
|
8
11
|
|
|
@@ -26,7 +29,7 @@ function compareText(left,right){
|
|
|
26
29
|
}
|
|
27
30
|
|
|
28
31
|
function safeRelativePath(value){
|
|
29
|
-
if(
|
|
32
|
+
if(!is.string(value)||!value||value.includes('\\')||value.includes('\0')
|
|
30
33
|
||path.posix.isAbsolute(value)||path.posix.normalize(value)!==value
|
|
31
34
|
||value==='.'||value.startsWith('../')||value.includes('/../')){
|
|
32
35
|
fail(`SDK browser runtime contains an unsafe relative path: ${String(value)}.`);
|
package/src/source-server.mjs
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import Is from 'strong-type';
|
|
1
2
|
import {createReadStream} from 'node:fs';
|
|
2
3
|
import {stat} from 'node:fs/promises';
|
|
3
4
|
import http from 'node:http';
|
|
@@ -6,6 +7,8 @@ import path from 'node:path';
|
|
|
6
7
|
import {pipeline} from 'node:stream/promises';
|
|
7
8
|
import {fileURLToPath} from 'node:url';
|
|
8
9
|
|
|
10
|
+
const is = new Is(false);
|
|
11
|
+
|
|
9
12
|
const MIME_TYPES = new Map([
|
|
10
13
|
['.css', 'text/css; charset=utf-8'],
|
|
11
14
|
['.gif', 'image/gif'],
|
|
@@ -61,7 +64,7 @@ function normalizeHost(value) {
|
|
|
61
64
|
|
|
62
65
|
function normalizePort(value) {
|
|
63
66
|
const port = Number(value ?? 0);
|
|
64
|
-
if (!
|
|
67
|
+
if (!is.integer(port) || port < 0 || port > 65535) {
|
|
65
68
|
fail('Source server port must be an integer from 0 through 65535.');
|
|
66
69
|
}
|
|
67
70
|
return port;
|
|
@@ -113,7 +116,7 @@ function normalizeRoot(value, fieldName) {
|
|
|
113
116
|
}
|
|
114
117
|
return path.resolve(fileURLToPath(value));
|
|
115
118
|
}
|
|
116
|
-
if (
|
|
119
|
+
if (!is.string(value) || !value.trim()) {
|
|
117
120
|
fail(`${fieldName} must be a filesystem path or file URL.`);
|
|
118
121
|
}
|
|
119
122
|
return path.resolve(value);
|
|
@@ -123,7 +126,7 @@ function normalizeInclude(value, fieldName) {
|
|
|
123
126
|
if (value === undefined) {
|
|
124
127
|
return null;
|
|
125
128
|
}
|
|
126
|
-
if (!
|
|
129
|
+
if (!is.array(value)) {
|
|
127
130
|
fail(`${fieldName} must be an array when supplied.`);
|
|
128
131
|
}
|
|
129
132
|
return value.map(function normalizeEntry(entry, index) {
|
|
@@ -132,7 +135,7 @@ function normalizeInclude(value, fieldName) {
|
|
|
132
135
|
}
|
|
133
136
|
|
|
134
137
|
function normalizeMount(value, index) {
|
|
135
|
-
if (!value ||
|
|
138
|
+
if (!value || !is.object(value) || is.array(value)) {
|
|
136
139
|
fail(`mounts[${index}] must be an object.`);
|
|
137
140
|
}
|
|
138
141
|
if (value.urlPath === undefined) {
|
|
@@ -149,7 +152,7 @@ function normalizeMount(value, index) {
|
|
|
149
152
|
}
|
|
150
153
|
|
|
151
154
|
function normalizeMounts(value) {
|
|
152
|
-
if (!
|
|
155
|
+
if (!is.array(value) || value.length === 0) {
|
|
153
156
|
fail('Source server mounts must be a nonempty array.');
|
|
154
157
|
}
|
|
155
158
|
const mounts = value.map(normalizeMount);
|
|
@@ -280,7 +283,7 @@ function parseRange(value, size) {
|
|
|
280
283
|
if (value === undefined) {
|
|
281
284
|
return null;
|
|
282
285
|
}
|
|
283
|
-
if (
|
|
286
|
+
if (!is.string(value) || size === 0) {
|
|
284
287
|
return false;
|
|
285
288
|
}
|
|
286
289
|
const match = /^bytes=(\d*)-(\d*)$/u.exec(value.trim());
|
|
@@ -291,7 +294,7 @@ function parseRange(value, size) {
|
|
|
291
294
|
let end;
|
|
292
295
|
if (!match[1]) {
|
|
293
296
|
const suffixLength = Number(match[2]);
|
|
294
|
-
if (!
|
|
297
|
+
if (!is.safeInteger(suffixLength) || suffixLength <= 0) {
|
|
295
298
|
return false;
|
|
296
299
|
}
|
|
297
300
|
start = Math.max(0, size - suffixLength);
|
|
@@ -300,7 +303,7 @@ function parseRange(value, size) {
|
|
|
300
303
|
else {
|
|
301
304
|
start = Number(match[1]);
|
|
302
305
|
end = match[2] ? Number(match[2]) : size - 1;
|
|
303
|
-
if (!
|
|
306
|
+
if (!is.safeInteger(start) || !is.safeInteger(end)
|
|
304
307
|
|| start < 0 || end < start || start >= size) {
|
|
305
308
|
return false;
|
|
306
309
|
}
|
|
@@ -344,7 +347,7 @@ async function streamFile(request, response, filePath, information, settings) {
|
|
|
344
347
|
}
|
|
345
348
|
|
|
346
349
|
async function publishEvent(onEvent, type, detail) {
|
|
347
|
-
if (
|
|
350
|
+
if (!is.function(onEvent)) {
|
|
348
351
|
return;
|
|
349
352
|
}
|
|
350
353
|
try {
|
|
@@ -419,7 +422,7 @@ function createServer(settings) {
|
|
|
419
422
|
if (settings.tls === undefined || settings.tls === null || settings.tls === false) {
|
|
420
423
|
return http.createServer(listener);
|
|
421
424
|
}
|
|
422
|
-
if (
|
|
425
|
+
if (!is.object(settings.tls) || is.array(settings.tls)) {
|
|
423
426
|
fail('Source server tls must be a Node HTTPS options object when supplied.');
|
|
424
427
|
}
|
|
425
428
|
return https.createServer(settings.tls, listener);
|
|
@@ -480,11 +483,11 @@ function closeFunction(server) {
|
|
|
480
483
|
}
|
|
481
484
|
|
|
482
485
|
export async function startSourceExampleServer(options = {}) {
|
|
483
|
-
if (!options ||
|
|
486
|
+
if (!options || !is.object(options) || is.array(options)) {
|
|
484
487
|
fail('Source server options must be an object.');
|
|
485
488
|
}
|
|
486
|
-
if (options.signal && (
|
|
487
|
-
||
|
|
489
|
+
if (options.signal && (!is.function(options.signal.addEventListener)
|
|
490
|
+
|| !is.function(options.signal.removeEventListener))) {
|
|
488
491
|
fail('Source server signal must be an AbortSignal when supplied.');
|
|
489
492
|
}
|
|
490
493
|
throwIfAborted(options.signal);
|
|
@@ -544,7 +547,7 @@ export async function startSourceExampleServer(options = {}) {
|
|
|
544
547
|
serverErrorListener = await listen(server, settings.host, settings.port,
|
|
545
548
|
reportOperationalError);
|
|
546
549
|
const address = server.address();
|
|
547
|
-
if (!address ||
|
|
550
|
+
if (!address || is.string(address)) {
|
|
548
551
|
await new Promise(function closeUnknownAddress(resolve) {
|
|
549
552
|
server.close(resolve);
|
|
550
553
|
});
|
package/src/targets/index.mjs
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import Is from 'strong-type';
|
|
1
2
|
import path from 'node:path';
|
|
2
3
|
import {packageApp,verifyApp} from '../packager/core.mjs';
|
|
3
4
|
import {startDevServer} from '../dev-server.mjs';
|
|
@@ -9,6 +10,8 @@ import {
|
|
|
9
10
|
validateNativeBuilder
|
|
10
11
|
} from '../native-plan.mjs';
|
|
11
12
|
|
|
13
|
+
const is = new Is(false);
|
|
14
|
+
|
|
12
15
|
const DEFINITIONS=[
|
|
13
16
|
{
|
|
14
17
|
id:'browser',displayName:'Browser/static',status:'available',
|
|
@@ -188,7 +191,7 @@ function selectedNativeRequest(targetId,targetRequest){
|
|
|
188
191
|
}
|
|
189
192
|
|
|
190
193
|
function selectedArtifact(artifact){
|
|
191
|
-
if(!artifact
|
|
194
|
+
if(!artifact||!is.object(artifact)||is.array(artifact)){
|
|
192
195
|
throw new ArcaneError(ERROR_CODES.usage,'A native artifact is required.');
|
|
193
196
|
}
|
|
194
197
|
return artifact;
|
|
@@ -202,7 +205,7 @@ export function createNativeTargetAdapter({targetId,nativeBuilder}={}){
|
|
|
202
205
|
async function requireProviderTarget(){
|
|
203
206
|
providerDescriptionPromise??=Promise.resolve(provider.describe()).then(description=>{
|
|
204
207
|
if(!description||description.protocol!=='arcane-native-builder/1'
|
|
205
|
-
||!
|
|
208
|
+
||!is.array(description.targets)||!description.targets.includes(targetId)){
|
|
206
209
|
throw new ArcaneError(
|
|
207
210
|
ERROR_CODES.targetUnavailable,
|
|
208
211
|
`The selected native provider does not support target ${targetId}.`
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import Is from 'strong-type';
|
|
1
2
|
import {
|
|
2
3
|
ARCANE_PROTOCOL,
|
|
3
4
|
CLI_EVENT_PROTOCOL,
|
|
@@ -6,6 +7,8 @@ import {
|
|
|
6
7
|
TARGET_ADAPTER_PROTOCOL
|
|
7
8
|
} from '../constants.mjs';
|
|
8
9
|
|
|
10
|
+
const is = new Is(false);
|
|
11
|
+
|
|
9
12
|
const NPM_PACKAGE_NAME_PATTERN=/^(?:@[a-z0-9][a-z0-9._-]*\/)?[a-z0-9][a-z0-9._-]*$/u;
|
|
10
13
|
const LOCAL_TARBALL_PATTERN=/^file:.+\.tgz$/iu;
|
|
11
14
|
|
|
@@ -34,9 +37,9 @@ export function createWorkspaceLockDocument({
|
|
|
34
37
|
packageVersion=SDK_VERSION,
|
|
35
38
|
packageSource=`node_modules/${dependencyName}`
|
|
36
39
|
}={}){
|
|
37
|
-
if(packageName!==SDK_NAME
|
|
40
|
+
if(packageName!==SDK_NAME||!is.string(packageVersion)
|
|
38
41
|
||!/^[0-9]+\.[0-9]+\.[0-9]+(?:-[0-9A-Za-z.-]+)?$/u.test(packageVersion)
|
|
39
|
-
|
|
42
|
+
||!is.string(dependencyName)||dependencyName.length>214
|
|
40
43
|
||!NPM_PACKAGE_NAME_PATTERN.test(dependencyName)
|
|
41
44
|
||packageSource!==`node_modules/${dependencyName}`){
|
|
42
45
|
throw new Error('Invalid workspace SDK lock authority.');
|
|
@@ -68,12 +71,12 @@ export function workspaceTemplate({
|
|
|
68
71
|
}){
|
|
69
72
|
const canonicalSpecifier=sdkDependencyName===SDK_NAME
|
|
70
73
|
&&(sdkDependencySpecifier===SDK_VERSION
|
|
71
|
-
||(
|
|
74
|
+
||(is.string(sdkDependencySpecifier)
|
|
72
75
|
&&LOCAL_TARBALL_PATTERN.test(sdkDependencySpecifier)
|
|
73
76
|
&&!/[\x00-\x1f\x7f]/.test(sdkDependencySpecifier)));
|
|
74
77
|
const aliasSpecifier=sdkDependencyName!==SDK_NAME
|
|
75
78
|
&&sdkDependencySpecifier===`npm:${SDK_NAME}@${SDK_VERSION}`;
|
|
76
|
-
if(
|
|
79
|
+
if(!is.string(sdkDependencyName)||sdkDependencyName.length>214
|
|
77
80
|
||!NPM_PACKAGE_NAME_PATTERN.test(sdkDependencyName)
|
|
78
81
|
||sdkPackageSource!==`node_modules/${sdkDependencyName}`
|
|
79
82
|
||(!canonicalSpecifier&&!aliasSpecifier)){
|
|
@@ -359,12 +362,16 @@ ${bootstrapMarkup}</head>
|
|
|
359
362
|
?'arcane/ThemeBootstrap':'../../../arcane/modules/ThemeBootstrap.js';
|
|
360
363
|
const appDataSpecifier=namedImports
|
|
361
364
|
?'arcane/AppDataScope':'../../../arcane/modules/AppDataScope.js';
|
|
362
|
-
|
|
365
|
+
const strongTypeSpecifier=namedImports
|
|
366
|
+
?'strong-type':'../../../arcane/dependencies/strong-type/index.js';
|
|
367
|
+
files.set(`apps/${appId}/modules/App.js`,`import Is from '${strongTypeSpecifier}';
|
|
368
|
+
import arcaneThemeReady from '${themeSpecifier}';
|
|
363
369
|
import {
|
|
364
370
|
resolveApplicationId,
|
|
365
371
|
resolveApplicationLocalStorageKey
|
|
366
372
|
} from '${appDataSpecifier}';
|
|
367
373
|
|
|
374
|
+
const is = new Is(false);
|
|
368
375
|
const appName=${JSON.stringify(name)};
|
|
369
376
|
const action=document.querySelector('#app-action');
|
|
370
377
|
const status=document.querySelector('#app-status');
|
|
@@ -377,7 +384,7 @@ const countKey=resolveApplicationLocalStorageKey('hello-count',{applicationId:ap
|
|
|
377
384
|
function loadHelloCount(){
|
|
378
385
|
try{
|
|
379
386
|
const value=Number(globalThis.localStorage?.getItem(countKey)??0);
|
|
380
|
-
return
|
|
387
|
+
return is.safeInteger(value)&&value>=0?value:0;
|
|
381
388
|
}catch{
|
|
382
389
|
return 0;
|
|
383
390
|
}
|
package/src/testing-loader.mjs
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
|
+
import Is from 'strong-type';
|
|
1
2
|
import {lstatSync,realpathSync} from 'node:fs';
|
|
2
3
|
import path from 'node:path';
|
|
3
4
|
import {fileURLToPath} from 'node:url';
|
|
4
5
|
|
|
6
|
+
const is = new Is(false);
|
|
7
|
+
|
|
5
8
|
const TESTING_SPECIFIER='arcane-os/testing';
|
|
6
9
|
const TESTING_URL=new URL('./testing.mjs',import.meta.url).href;
|
|
7
10
|
const RUNTIME_STRONG_TYPE_SPECIFIER='../../node_modules/strong-type/index.js';
|
|
@@ -25,7 +28,7 @@ function urlLikeSpecifier(specifier){
|
|
|
25
28
|
}
|
|
26
29
|
|
|
27
30
|
function safeManagedTarget(target){
|
|
28
|
-
return
|
|
31
|
+
return is.string(target)
|
|
29
32
|
&&target.startsWith('./')
|
|
30
33
|
&&target.length>2
|
|
31
34
|
&&!/[\\%?#\u0000-\u001f\u007f]/u.test(target)
|
|
@@ -115,14 +118,14 @@ export function initialize({managedImportMap}={}){
|
|
|
115
118
|
const imports=managedImportMap.imports;
|
|
116
119
|
if(managedImportMap.protocol!==MANAGED_IMPORT_MAP_PROTOCOL
|
|
117
120
|
||!MANAGED_IMPORT_MAP_BOUNDARIES.has(managedImportMap.boundary)
|
|
118
|
-
||imports===null
|
|
121
|
+
||imports===null||!is.object(imports)||is.array(imports)){
|
|
119
122
|
loaderFailure('Managed test import-map loader data is malformed.');
|
|
120
123
|
}
|
|
121
124
|
const boundary=physicalDirectory(managedImportMap.baseURL);
|
|
122
125
|
const exact=new Map();
|
|
123
126
|
const urls=new Map();
|
|
124
127
|
for(const [specifier,target] of Object.entries(imports)){
|
|
125
|
-
if(
|
|
128
|
+
if(!is.string(specifier)||specifier===''||!safeManagedTarget(target)){
|
|
126
129
|
loaderFailure(`Managed test import-map entry is invalid: ${String(specifier)}.`);
|
|
127
130
|
}
|
|
128
131
|
const targetURL=physicalManagedTarget(
|
|
@@ -149,7 +152,7 @@ export function initialize({managedImportMap}={}){
|
|
|
149
152
|
function managedResolution(specifier,context){
|
|
150
153
|
if(managedImports===null)return null;
|
|
151
154
|
if(managedImports.has(specifier))return managedImports.get(specifier);
|
|
152
|
-
if(urlLikeSpecifier(specifier)&&
|
|
155
|
+
if(urlLikeSpecifier(specifier)&&is.string(context.parentURL)){
|
|
153
156
|
let requested;
|
|
154
157
|
try{requested=new URL(specifier,context.parentURL).href;}
|
|
155
158
|
catch{requested=null;}
|