arcane-os 0.3.2 → 0.3.4
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 +23 -0
- package/README.md +7 -7
- package/package.json +1 -1
- package/runtime/arcane/components/speech.html +75 -18
- package/schemas/arcane-lock.schema.json +4 -1
- package/src/cli/main.mjs +12 -11
- package/src/dev-server.mjs +13 -4
- package/src/installed-sdk-runtime.mjs +24 -2
- package/src/mail-server.mjs +12 -5
- package/src/mail.mjs +25 -19
- package/src/packager/core.mjs +12 -5
- package/src/scaffold.mjs +19 -0
- package/src/templates/workspace-template.mjs +37 -1
- package/src/toolchain.mjs +3 -1
- package/src/workspace.mjs +32 -9
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,28 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.3.4
|
|
4
|
+
|
|
5
|
+
- Allowed external and integrated physical SDK workspaces to omit the optional
|
|
6
|
+
`security` runtime route while preserving the canonical functional route
|
|
7
|
+
order, `dependencies` and `sdk` projections, external license routing, and
|
|
8
|
+
compatibility with workspaces that still include `security`.
|
|
9
|
+
- Preserved a newer microphone retry's press, status, and operation identity
|
|
10
|
+
when an earlier pending capture request settles, while retaining the original
|
|
11
|
+
operation correlation for successful transcription.
|
|
12
|
+
|
|
13
|
+
## 0.3.3
|
|
14
|
+
|
|
15
|
+
- Corrected installed-workspace import-map refresh to preserve rich browser
|
|
16
|
+
document inspection records while passing only navigable document paths to
|
|
17
|
+
the public generator, including `.html` and `.htm` pages without modifying
|
|
18
|
+
HTML fragments.
|
|
19
|
+
- Restored one semantic `arcane.lock.json` contract across scaffold, init, and
|
|
20
|
+
installed runtime materialization, replacing stale versions with the exact
|
|
21
|
+
installed package version and npm-alias roots without byte or security
|
|
22
|
+
metadata.
|
|
23
|
+
- Added selected installed-package coverage for stale-lock replacement and the
|
|
24
|
+
public multi-document import-map command through the packed npm artifact.
|
|
25
|
+
|
|
3
26
|
## 0.3.2
|
|
4
27
|
|
|
5
28
|
- Preserved complete application, document, model, message, event, process,
|
package/README.md
CHANGED
|
@@ -19,7 +19,7 @@ version-locked SDK runtime, while an integrated Arcane checkout uses its live
|
|
|
19
19
|
`arcane/` runtime. Both profiles preserve the same app URLs, theme, packaging,
|
|
20
20
|
event, cancellation, and browser run contracts.
|
|
21
21
|
|
|
22
|
-
This checkout defines the `0.3.
|
|
22
|
+
This checkout defines the `0.3.4` SDK contract. Applications pin one exact npm
|
|
23
23
|
version and lockfile; registry state is deliberately not baked into application
|
|
24
24
|
artifacts.
|
|
25
25
|
|
|
@@ -126,7 +126,7 @@ uses the same controller for automatic memory extraction.
|
|
|
126
126
|
Create a new repository-shaped Arcane application with the exact stable SDK:
|
|
127
127
|
|
|
128
128
|
```bash
|
|
129
|
-
npx arcane-os@0.3.
|
|
129
|
+
npx arcane-os@0.3.4 new my-app --path ./my-app --target portable --git
|
|
130
130
|
cd my-app
|
|
131
131
|
npm install
|
|
132
132
|
npm run check
|
|
@@ -137,7 +137,7 @@ To enroll an existing repository, install the exact SDK and initialize only
|
|
|
137
137
|
missing Arcane files:
|
|
138
138
|
|
|
139
139
|
```bash
|
|
140
|
-
npm install --save-dev --save-exact arcane-os@0.3.
|
|
140
|
+
npm install --save-dev --save-exact arcane-os@0.3.4
|
|
141
141
|
npm exec -- arcane init my-app --target portable
|
|
142
142
|
```
|
|
143
143
|
|
|
@@ -153,7 +153,7 @@ npm exec -- arcane-os targets
|
|
|
153
153
|
No global SDK install or standalone Arcane CLI is required. The application
|
|
154
154
|
repository's exact npm dependency and lockfile own the CLI and toolchain version.
|
|
155
155
|
|
|
156
|
-
Use `npx arcane-os@0.3.
|
|
156
|
+
Use `npx arcane-os@0.3.4` for the initial bootstrap because it names this npm
|
|
157
157
|
package explicitly; bare `npx arcane` outside an installed project could resolve
|
|
158
158
|
a different package. Both installed commands invoke the same headless toolchain.
|
|
159
159
|
Project-local npm scripts use the SDK pinned by that app's `package-lock.json`,
|
|
@@ -174,7 +174,7 @@ node ./bin/arcane.mjs new local-app --path ../local-app --target portable --git
|
|
|
174
174
|
|
|
175
175
|
# From the generated app repository
|
|
176
176
|
cd ../local-app
|
|
177
|
-
npm install --save-dev --save-exact ../arcane-os-sdk/arcane-os-0.3.
|
|
177
|
+
npm install --save-dev --save-exact ../arcane-os-sdk/arcane-os-0.3.4.tgz
|
|
178
178
|
npm run check
|
|
179
179
|
npm ci
|
|
180
180
|
```
|
|
@@ -184,7 +184,7 @@ same location. The lockfile retains the selected package dependency while
|
|
|
184
184
|
Arcane uses the installed package name and version. Local directory `file:` dependencies are not
|
|
185
185
|
accepted because npm may install them as links; use a packed `.tgz`. A GitHub
|
|
186
186
|
runner also needs that tarball at the locked path. After publication, replace
|
|
187
|
-
the local declaration with the exact `arcane-os@0.3.
|
|
187
|
+
the local declaration with the exact `arcane-os@0.3.4` registry package and
|
|
188
188
|
commit the regenerated lock.
|
|
189
189
|
|
|
190
190
|
Generated repositories use `npm ci --ignore-scripts` in CI. Run dependency
|
|
@@ -322,7 +322,7 @@ package installation, or assertions.
|
|
|
322
322
|
|
|
323
323
|
## Current target support
|
|
324
324
|
|
|
325
|
-
Version `0.3.
|
|
325
|
+
Version `0.3.4` exposes one browser target and five explicitly paired
|
|
326
326
|
native development targets: a non-runnable portable directory, a
|
|
327
327
|
Windows x64 unsigned-local-test EXE bundle, Linux x64 and Linux ARM64
|
|
328
328
|
unsigned-local-test DEBs, and an Android development-signed APK. The
|
package/package.json
CHANGED
|
@@ -1235,11 +1235,14 @@
|
|
|
1235
1235
|
}
|
|
1236
1236
|
|
|
1237
1237
|
const generation = ++recordingGeneration;
|
|
1238
|
-
|
|
1238
|
+
const operationId = nextSpeechOperationId('transcription');
|
|
1239
|
+
transcriptionOperationId = operationId;
|
|
1239
1240
|
recordingRequested = true;
|
|
1240
1241
|
localStatus = {
|
|
1241
1242
|
message: 'Recording. Release to transcribe.',
|
|
1242
|
-
tone: ''
|
|
1243
|
+
tone: '',
|
|
1244
|
+
owner: 'recording',
|
|
1245
|
+
generation
|
|
1243
1246
|
};
|
|
1244
1247
|
renderControls();
|
|
1245
1248
|
renderStatus();
|
|
@@ -1248,6 +1251,9 @@
|
|
|
1248
1251
|
try {
|
|
1249
1252
|
stream = await navigator.mediaDevices.getUserMedia({ audio: true });
|
|
1250
1253
|
} catch (error) {
|
|
1254
|
+
if (generation !== recordingGeneration || destroyed) {
|
|
1255
|
+
return false;
|
|
1256
|
+
}
|
|
1251
1257
|
recordingRequested = false;
|
|
1252
1258
|
resetRecordPress();
|
|
1253
1259
|
if (
|
|
@@ -1268,7 +1274,7 @@
|
|
|
1268
1274
|
{
|
|
1269
1275
|
bubbles: true,
|
|
1270
1276
|
composed: true,
|
|
1271
|
-
operationId
|
|
1277
|
+
operationId,
|
|
1272
1278
|
publicDetail: {
|
|
1273
1279
|
...publicSpeechErrorFields(
|
|
1274
1280
|
error,
|
|
@@ -1278,7 +1284,9 @@
|
|
|
1278
1284
|
}
|
|
1279
1285
|
}
|
|
1280
1286
|
);
|
|
1281
|
-
transcriptionOperationId
|
|
1287
|
+
if (transcriptionOperationId === operationId) {
|
|
1288
|
+
transcriptionOperationId = null;
|
|
1289
|
+
}
|
|
1282
1290
|
} else {
|
|
1283
1291
|
reportTranscriptionError(error);
|
|
1284
1292
|
}
|
|
@@ -1287,15 +1295,25 @@
|
|
|
1287
1295
|
return false;
|
|
1288
1296
|
}
|
|
1289
1297
|
|
|
1298
|
+
const currentGeneration = generation === recordingGeneration;
|
|
1290
1299
|
if (
|
|
1291
1300
|
!recordingRequested
|
|
1292
|
-
||
|
|
1301
|
+
|| !currentGeneration
|
|
1293
1302
|
|| sttRole.state !== 'ready'
|
|
1294
1303
|
|| sttRole.busy
|
|
1295
1304
|
|| destroyed
|
|
1296
1305
|
) {
|
|
1297
1306
|
stopMediaStream(stream);
|
|
1298
|
-
|
|
1307
|
+
if (currentGeneration) {
|
|
1308
|
+
recordingRequested = false;
|
|
1309
|
+
resetRecordPress();
|
|
1310
|
+
clearRecordingStatus(generation);
|
|
1311
|
+
if (transcriptionOperationId === operationId) {
|
|
1312
|
+
transcriptionOperationId = null;
|
|
1313
|
+
}
|
|
1314
|
+
renderControls();
|
|
1315
|
+
renderStatus();
|
|
1316
|
+
}
|
|
1299
1317
|
return false;
|
|
1300
1318
|
}
|
|
1301
1319
|
|
|
@@ -1311,6 +1329,7 @@
|
|
|
1311
1329
|
const processor = audioContext.createScriptProcessor(4096, 1, 1);
|
|
1312
1330
|
session = {
|
|
1313
1331
|
generation,
|
|
1332
|
+
operationId,
|
|
1314
1333
|
recorder,
|
|
1315
1334
|
stream,
|
|
1316
1335
|
audioContext,
|
|
@@ -1349,7 +1368,6 @@
|
|
|
1349
1368
|
recorder.start();
|
|
1350
1369
|
return true;
|
|
1351
1370
|
} catch (error) {
|
|
1352
|
-
recordingRequested = false;
|
|
1353
1371
|
if (session) {
|
|
1354
1372
|
session.cancelled = true;
|
|
1355
1373
|
releaseCaptureSession(session);
|
|
@@ -1359,8 +1377,11 @@
|
|
|
1359
1377
|
} else {
|
|
1360
1378
|
stopMediaStream(stream);
|
|
1361
1379
|
}
|
|
1362
|
-
|
|
1363
|
-
|
|
1380
|
+
if (generation === recordingGeneration) {
|
|
1381
|
+
recordingRequested = false;
|
|
1382
|
+
resetRecordPress();
|
|
1383
|
+
reportTranscriptionError(error);
|
|
1384
|
+
}
|
|
1364
1385
|
return false;
|
|
1365
1386
|
}
|
|
1366
1387
|
}
|
|
@@ -1377,32 +1398,67 @@
|
|
|
1377
1398
|
}
|
|
1378
1399
|
}
|
|
1379
1400
|
|
|
1401
|
+
function clearRecordingStatus(generation) {
|
|
1402
|
+
if (
|
|
1403
|
+
localStatus?.owner === 'recording'
|
|
1404
|
+
&& localStatus.generation === generation
|
|
1405
|
+
) {
|
|
1406
|
+
localStatus = null;
|
|
1407
|
+
}
|
|
1408
|
+
}
|
|
1409
|
+
|
|
1380
1410
|
function stop() {
|
|
1411
|
+
const pendingCaptureStart = recordingRequested && !captureSession;
|
|
1412
|
+
const generation = recordingGeneration;
|
|
1413
|
+
const operationId = transcriptionOperationId;
|
|
1381
1414
|
recordingRequested = false;
|
|
1382
1415
|
clearSilenceTimer();
|
|
1383
|
-
if (
|
|
1416
|
+
if (pendingCaptureStart) {
|
|
1417
|
+
// getUserMedia cannot be cancelled synchronously. Retire this
|
|
1418
|
+
// generation so its eventual settlement cannot touch a retry.
|
|
1419
|
+
recordingGeneration += 1;
|
|
1420
|
+
resetRecordPress();
|
|
1421
|
+
clearRecordingStatus(generation);
|
|
1422
|
+
if (transcriptionOperationId === operationId) {
|
|
1423
|
+
transcriptionOperationId = null;
|
|
1424
|
+
}
|
|
1425
|
+
renderControls();
|
|
1426
|
+
renderStatus();
|
|
1427
|
+
return true;
|
|
1428
|
+
}
|
|
1429
|
+
if (captureSession && captureSession.recorder.state !== 'inactive') {
|
|
1384
1430
|
captureSession.recorder.stop();
|
|
1385
1431
|
}
|
|
1386
1432
|
return true;
|
|
1387
1433
|
}
|
|
1388
1434
|
|
|
1389
1435
|
async function finalizeCapture(session) {
|
|
1436
|
+
const currentGeneration = session.generation === recordingGeneration;
|
|
1437
|
+
const currentOperation = transcriptionOperationId === session.operationId;
|
|
1390
1438
|
releaseCaptureSession(session);
|
|
1391
1439
|
if (captureSession === session) {
|
|
1392
1440
|
captureSession = null;
|
|
1393
1441
|
}
|
|
1394
|
-
|
|
1395
|
-
|
|
1442
|
+
if (currentGeneration) {
|
|
1443
|
+
recordingRequested = false;
|
|
1444
|
+
resetRecordPress();
|
|
1445
|
+
}
|
|
1396
1446
|
|
|
1397
1447
|
if (
|
|
1398
1448
|
session.cancelled
|
|
1399
|
-
||
|
|
1449
|
+
|| !currentGeneration
|
|
1400
1450
|
|| sttRole.state !== 'ready'
|
|
1401
1451
|
|| sttRole.busy
|
|
1402
1452
|
|| destroyed
|
|
1403
1453
|
) {
|
|
1404
|
-
|
|
1405
|
-
|
|
1454
|
+
if (currentGeneration) {
|
|
1455
|
+
clearRecordingStatus(session.generation);
|
|
1456
|
+
if (currentOperation) {
|
|
1457
|
+
transcriptionOperationId = null;
|
|
1458
|
+
}
|
|
1459
|
+
renderControls();
|
|
1460
|
+
renderStatus();
|
|
1461
|
+
}
|
|
1406
1462
|
return false;
|
|
1407
1463
|
}
|
|
1408
1464
|
|
|
@@ -1415,12 +1471,13 @@
|
|
|
1415
1471
|
'audio.webm',
|
|
1416
1472
|
{ type: 'audio/webm' }
|
|
1417
1473
|
);
|
|
1418
|
-
return transcribeAudio(audioFile);
|
|
1474
|
+
return transcribeAudio(audioFile, session.operationId);
|
|
1419
1475
|
}
|
|
1420
1476
|
|
|
1421
|
-
async function transcribeAudio(audioFile) {
|
|
1477
|
+
async function transcribeAudio(audioFile, operationId = null) {
|
|
1422
1478
|
const generation = ++transcriptionGeneration;
|
|
1423
|
-
transcriptionOperationId =
|
|
1479
|
+
transcriptionOperationId = operationId
|
|
1480
|
+
|| transcriptionOperationId
|
|
1424
1481
|
|| nextSpeechOperationId('transcription');
|
|
1425
1482
|
const controller = new AbortController();
|
|
1426
1483
|
transcriptionAbortController?.abort();
|
|
@@ -14,7 +14,10 @@
|
|
|
14
14
|
"required": ["name", "version"],
|
|
15
15
|
"properties": {
|
|
16
16
|
"name": {"const": "arcane-os"},
|
|
17
|
-
"version": {
|
|
17
|
+
"version": {
|
|
18
|
+
"type": "string",
|
|
19
|
+
"pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+(?:-[0-9A-Za-z.-]+)?$"
|
|
20
|
+
}
|
|
18
21
|
}
|
|
19
22
|
},
|
|
20
23
|
"runtime": {
|
package/src/cli/main.mjs
CHANGED
|
@@ -47,6 +47,7 @@ const REPORTABLE_COMMANDS=new Set([
|
|
|
47
47
|
'native-doctor','native-prepare','new','package','repo','run','targets','test',
|
|
48
48
|
'update-check','upgrade','verify','verify-bundle','version'
|
|
49
49
|
]);
|
|
50
|
+
const MAX_NODE_TIMER_DELAY_MS=2_147_483_647;
|
|
50
51
|
|
|
51
52
|
export const HELP_TEXT=`Arcane OS application SDK ${SDK_VERSION}
|
|
52
53
|
|
|
@@ -76,7 +77,7 @@ Usage:
|
|
|
76
77
|
${CLI_NAME} mail key status <profile>
|
|
77
78
|
${CLI_NAME} mail key delete <profile>
|
|
78
79
|
${CLI_NAME} mail send --profile <profile> --from <address> --report-key <id> --report-stdin [--request-timeout <ms>]
|
|
79
|
-
${CLI_NAME} mail serve --profile <profile> --from <address> --app <id> --origin <origin> --allow-to <addresses> [--app-key-stdin] [--host 127.0.0.1] [--port 8025] [--request-timeout <ms>]
|
|
80
|
+
${CLI_NAME} mail serve --profile <profile> --from <address> --app <id> --origin <origin> [--allow-to <addresses>] [--app-key-stdin] [--host 127.0.0.1] [--port 8025] [--request-timeout <ms>]
|
|
80
81
|
|
|
81
82
|
Development:
|
|
82
83
|
--sdk-runtime-source <sdk-root> Dev-only live SDK checkout; omitted preserves the workspace runtime mode.
|
|
@@ -174,16 +175,17 @@ function readPort(value,defaultValue){
|
|
|
174
175
|
return port;
|
|
175
176
|
}
|
|
176
177
|
|
|
177
|
-
function readRequestTimeout(value
|
|
178
|
-
if(value===undefined)
|
|
179
|
-
return defaultValue;
|
|
180
|
-
}
|
|
178
|
+
function readRequestTimeout(value){
|
|
179
|
+
if(value===undefined)return undefined;
|
|
181
180
|
if(!/^\d+$/u.test(value)){
|
|
182
181
|
usage(`Invalid request timeout: ${value}.`);
|
|
183
182
|
}
|
|
184
183
|
const timeout=Number(value);
|
|
185
|
-
if(!Number.isSafeInteger(timeout)||timeout<
|
|
186
|
-
usage(
|
|
184
|
+
if(!Number.isSafeInteger(timeout)||timeout<1||timeout>MAX_NODE_TIMER_DELAY_MS){
|
|
185
|
+
usage(
|
|
186
|
+
`Mail request timeout must be an integer from 1 through ${MAX_NODE_TIMER_DELAY_MS} `
|
|
187
|
+
+'milliseconds, the Node timer range.'
|
|
188
|
+
);
|
|
187
189
|
}
|
|
188
190
|
return timeout;
|
|
189
191
|
}
|
|
@@ -649,8 +651,7 @@ function operationOptions(command,parsed,cwd){
|
|
|
649
651
|
profile:values.profile,
|
|
650
652
|
from:values.from,
|
|
651
653
|
app:values.app,
|
|
652
|
-
origin:values.origin
|
|
653
|
-
'allow-to':values['allow-to'],
|
|
654
|
+
origin:values.origin
|
|
654
655
|
})){
|
|
655
656
|
if(!value)usage(`mail serve requires --${name} <value>.`);
|
|
656
657
|
}
|
|
@@ -664,7 +665,7 @@ function operationOptions(command,parsed,cwd){
|
|
|
664
665
|
appKeyStdin:flags.has('app-key-stdin'),
|
|
665
666
|
host:values.host??'127.0.0.1',
|
|
666
667
|
port:readPort(values.port,8025),
|
|
667
|
-
requestTimeout:readRequestTimeout(values['request-timeout']
|
|
668
|
+
requestTimeout:readRequestTimeout(values['request-timeout']),
|
|
668
669
|
};
|
|
669
670
|
}
|
|
670
671
|
if(area==='send'){
|
|
@@ -693,7 +694,7 @@ function operationOptions(command,parsed,cwd){
|
|
|
693
694
|
from:values.from,
|
|
694
695
|
reportKey:values['report-key'],
|
|
695
696
|
reportStdin:true,
|
|
696
|
-
requestTimeout:readRequestTimeout(values['request-timeout']
|
|
697
|
+
requestTimeout:readRequestTimeout(values['request-timeout']),
|
|
697
698
|
};
|
|
698
699
|
}
|
|
699
700
|
usage('mail requires key set|status|delete <profile>, send, or serve.');
|
package/src/dev-server.mjs
CHANGED
|
@@ -31,6 +31,11 @@ const SDK_RUNTIME_SOURCE_PROTOCOL='arcane-sdk-runtime-source/1';
|
|
|
31
31
|
const SDK_RUNTIME_SOURCE_ARCANE_ROOTS=new Set([
|
|
32
32
|
'components','css','entities','img','modules','security'
|
|
33
33
|
]);
|
|
34
|
+
const SDK_INSTALLED_ARCANE_ROOTS=new Set([
|
|
35
|
+
...SDK_RUNTIME_SOURCE_ARCANE_ROOTS,
|
|
36
|
+
'dependencies',
|
|
37
|
+
'sdk'
|
|
38
|
+
]);
|
|
34
39
|
const SDK_RUNTIME_SOURCE_PRIVATE_SEGMENTS=new Set([
|
|
35
40
|
'node_modules','.git','.hg','.svn','cvs'
|
|
36
41
|
]);
|
|
@@ -126,7 +131,7 @@ async function canonicalRealDirectory(requested,label){
|
|
|
126
131
|
return canonical;
|
|
127
132
|
}
|
|
128
133
|
|
|
129
|
-
function sdkRuntimeSourcePathAllowed(relative,{
|
|
134
|
+
function sdkRuntimeSourcePathAllowed(relative,{arcaneRoots=null}={}){
|
|
130
135
|
if(relative.length===0)return false;
|
|
131
136
|
const normalized=relative.map(function normalizeRuntimeSourceSegment(segment){
|
|
132
137
|
return segment.normalize('NFC').toLowerCase();
|
|
@@ -135,11 +140,15 @@ function sdkRuntimeSourcePathAllowed(relative,{arcaneRoot=false}={}){
|
|
|
135
140
|
return segment.startsWith('.')||SDK_RUNTIME_SOURCE_PRIVATE_SEGMENTS.has(segment)
|
|
136
141
|
||SDK_RUNTIME_SOURCE_PRIVATE_MANIFESTS.has(segment);
|
|
137
142
|
}))return false;
|
|
138
|
-
return
|
|
143
|
+
return arcaneRoots===null||arcaneRoots.has(normalized[0]);
|
|
139
144
|
}
|
|
140
145
|
|
|
141
146
|
function sdkArcaneSourcePathAllowed(relative){
|
|
142
|
-
return sdkRuntimeSourcePathAllowed(relative,{
|
|
147
|
+
return sdkRuntimeSourcePathAllowed(relative,{arcaneRoots:SDK_RUNTIME_SOURCE_ARCANE_ROOTS});
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function sdkInstalledArcanePathAllowed(relative){
|
|
151
|
+
return sdkRuntimeSourcePathAllowed(relative,{arcaneRoots:SDK_INSTALLED_ARCANE_ROOTS});
|
|
143
152
|
}
|
|
144
153
|
|
|
145
154
|
function sdkDependencySourcePathAllowed(relative){
|
|
@@ -418,7 +427,7 @@ async function sourceRoutes(workspaceRoot,appId,{
|
|
|
418
427
|
{
|
|
419
428
|
prefix:['arcane'],
|
|
420
429
|
root:runtimeRoot,
|
|
421
|
-
allow:
|
|
430
|
+
allow:sdkInstalledArcanePathAllowed
|
|
422
431
|
}
|
|
423
432
|
]
|
|
424
433
|
};
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import {readFile,realpath,stat} from 'node:fs/promises';
|
|
1
|
+
import {lstat,readFile,realpath,stat,writeFile} from 'node:fs/promises';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import {materializeWorkspaceRuntimeContent} from './workspace-runtime.mjs';
|
|
4
4
|
import {withWorkspaceOperationLock} from './workspace-operation-lock.mjs';
|
|
5
|
+
import {createWorkspaceLockDocument} from './templates/workspace-template.mjs';
|
|
5
6
|
import {
|
|
6
7
|
resolveInstalledSdkInstallation,
|
|
7
8
|
resolveSdkPackageDeclaration
|
|
@@ -55,6 +56,18 @@ async function installedSdkAuthority(canonicalRoot,{sdkPackageSource}={}){
|
|
|
55
56
|
return {declaration,installation};
|
|
56
57
|
}
|
|
57
58
|
|
|
59
|
+
async function writeWorkspaceLock(lockPath,lock){
|
|
60
|
+
try{
|
|
61
|
+
const info=await lstat(lockPath);
|
|
62
|
+
if(info.isSymbolicLink()||!info.isFile()){
|
|
63
|
+
fail(`Workspace SDK lock must be a real file: ${lockPath}.`);
|
|
64
|
+
}
|
|
65
|
+
}catch(error){
|
|
66
|
+
if(error?.code!=='ENOENT')throw error;
|
|
67
|
+
}
|
|
68
|
+
await writeFile(lockPath,`${JSON.stringify(lock,null,2)}\n`,'utf8');
|
|
69
|
+
}
|
|
70
|
+
|
|
58
71
|
export async function materializeInstalledSdkRuntime({
|
|
59
72
|
workspaceRoot,
|
|
60
73
|
sdkPackageSource,
|
|
@@ -79,12 +92,21 @@ export async function materializeInstalledSdkRuntime({
|
|
|
79
92
|
signal,
|
|
80
93
|
onEvent
|
|
81
94
|
});
|
|
95
|
+
const lockPath=path.join(canonicalRoot,'arcane.lock.json');
|
|
96
|
+
const lock=createWorkspaceLockDocument({
|
|
97
|
+
dependencyName:authority.installation.dependencyName,
|
|
98
|
+
packageName:authority.installation.packageName,
|
|
99
|
+
packageVersion:authority.installation.packageVersion,
|
|
100
|
+
packageSource:authority.installation.packageSource
|
|
101
|
+
});
|
|
102
|
+
await writeWorkspaceLock(lockPath,lock);
|
|
82
103
|
return {
|
|
83
104
|
schemaVersion:1,
|
|
84
105
|
kind:'arcane-installed-sdk-runtime-materialization',
|
|
85
106
|
status:'materialized',
|
|
86
107
|
installation:authority.installation,
|
|
87
|
-
workspaceRuntime
|
|
108
|
+
workspaceRuntime,
|
|
109
|
+
workspaceLock:{path:lockPath,document:lock}
|
|
88
110
|
};
|
|
89
111
|
});
|
|
90
112
|
}
|
package/src/mail-server.mjs
CHANGED
|
@@ -19,6 +19,7 @@ const PREFLIGHT_HEADERS=new Set([
|
|
|
19
19
|
]);
|
|
20
20
|
const PERMANENT_RATE_CODES=new Set(['daily_quota_exceeded','monthly_quota_exceeded']);
|
|
21
21
|
const RETRYABLE_PROVIDER_STATUSES=new Set([408,425,429,500,502,503,504]);
|
|
22
|
+
const MAX_NODE_TIMER_DELAY_MS=2_147_483_647;
|
|
22
23
|
|
|
23
24
|
class MailGatewayFault extends Error {
|
|
24
25
|
constructor(code,{details=null,retryable=false,retryAfterMs=0,statusCode=400,uncertain=false}={}){
|
|
@@ -61,9 +62,15 @@ function positiveInteger(value,fallback,{label,allowZero=false}={}){
|
|
|
61
62
|
return resolved;
|
|
62
63
|
}
|
|
63
64
|
|
|
64
|
-
function
|
|
65
|
+
function optionalTimerDelay(value,label){
|
|
65
66
|
if(value===undefined||value===null) return null;
|
|
66
|
-
|
|
67
|
+
if(!Number.isSafeInteger(value)||value<1||value>MAX_NODE_TIMER_DELAY_MS){
|
|
68
|
+
throw configurationError(
|
|
69
|
+
`${label} must be an integer from 1 through ${MAX_NODE_TIMER_DELAY_MS} `
|
|
70
|
+
+'milliseconds, the Node timer range.'
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
return value;
|
|
67
74
|
}
|
|
68
75
|
|
|
69
76
|
function normalizeRetryAfter(value){
|
|
@@ -248,7 +255,7 @@ function normalizeConfiguration(options={}){
|
|
|
248
255
|
apiKey:validateApiKey(options.apiKey),
|
|
249
256
|
appKeyDigest:callerAuthentication.appKeyDigest,
|
|
250
257
|
appId:validateAppId(options.appId),
|
|
251
|
-
bodyTimeoutMs:
|
|
258
|
+
bodyTimeoutMs:optionalTimerDelay(options.bodyTimeoutMs,'bodyTimeoutMs'),
|
|
252
259
|
errorRecipients,
|
|
253
260
|
fetchImpl,
|
|
254
261
|
from:validateFrom(options.from),
|
|
@@ -256,7 +263,7 @@ function normalizeConfiguration(options={}){
|
|
|
256
263
|
callerAuthentication:callerAuthentication.callerAuthentication,
|
|
257
264
|
onEvent:options.onEvent,
|
|
258
265
|
port:portNumber(options.port,8025),
|
|
259
|
-
providerTimeoutMs:
|
|
266
|
+
providerTimeoutMs:optionalTimerDelay(options.providerTimeoutMs,'providerTimeoutMs'),
|
|
260
267
|
requestIdFactory:options.requestIdFactory??randomUUID,
|
|
261
268
|
retryableDelayMs:positiveInteger(
|
|
262
269
|
options.retryableDelayMs,
|
|
@@ -968,7 +975,7 @@ function normalizeDirectSendOptions(options){
|
|
|
968
975
|
errorRecipients:[],
|
|
969
976
|
fetchImpl,
|
|
970
977
|
from:validateFrom(options.from),
|
|
971
|
-
providerTimeoutMs:
|
|
978
|
+
providerTimeoutMs:optionalTimerDelay(options.providerTimeoutMs,'providerTimeoutMs'),
|
|
972
979
|
requestIdFactory:options.requestIdFactory??randomUUID,
|
|
973
980
|
retryableDelayMs:positiveInteger(
|
|
974
981
|
options.retryableDelayMs,
|
package/src/mail.mjs
CHANGED
|
@@ -40,13 +40,16 @@ function dependency(options,name,fallback){
|
|
|
40
40
|
}
|
|
41
41
|
|
|
42
42
|
function recipientList(value,label){
|
|
43
|
+
if(value===undefined||value===null||value===''||(Array.isArray(value)&&value.length===0)){
|
|
44
|
+
return [];
|
|
45
|
+
}
|
|
43
46
|
const entries=Array.isArray(value)
|
|
44
47
|
? value
|
|
45
48
|
: typeof value==='string'
|
|
46
49
|
? value.split(',')
|
|
47
50
|
: null;
|
|
48
|
-
if(!entries||entries.length===0
|
|
49
|
-
usage(`${label} must contain one
|
|
51
|
+
if(!entries||entries.length===0){
|
|
52
|
+
usage(`${label} must contain at least one email address when supplied.`);
|
|
50
53
|
}
|
|
51
54
|
const result=entries.map(function normalizeMailRecipient(entry){
|
|
52
55
|
if(typeof entry!=='string'||!entry.trim()){
|
|
@@ -111,21 +114,24 @@ async function deleteCredential(options){
|
|
|
111
114
|
return remove(credentialOptions(options));
|
|
112
115
|
}
|
|
113
116
|
|
|
114
|
-
function
|
|
115
|
-
return
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
117
|
+
function withoutMailCredentials(value,seen=new WeakMap()){
|
|
118
|
+
if(!value||typeof value!=='object')return value;
|
|
119
|
+
if(seen.has(value))return seen.get(value);
|
|
120
|
+
const copy=Array.isArray(value)?[]:{};
|
|
121
|
+
seen.set(value,copy);
|
|
122
|
+
for(const [key,entry] of Object.entries(value)){
|
|
123
|
+
if(key==='apiKey'||key==='appKey')continue;
|
|
124
|
+
copy[key]=withoutMailCredentials(entry,seen);
|
|
125
|
+
}
|
|
126
|
+
return copy;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function completeSendFailure(result){
|
|
130
|
+
if(!result||typeof result!=='object'||Array.isArray(result)){
|
|
131
|
+
return {provider:'resend',result:withoutMailCredentials(result)};
|
|
132
|
+
}
|
|
133
|
+
// Complete provider detail remains visible; only credential fields are omitted.
|
|
134
|
+
return withoutMailCredentials(result);
|
|
129
135
|
}
|
|
130
136
|
|
|
131
137
|
async function sendMail(options){
|
|
@@ -164,9 +170,9 @@ async function sendMail(options){
|
|
|
164
170
|
signal:options.signal
|
|
165
171
|
});
|
|
166
172
|
if(result?.classification==='accepted'&&result.status==='accepted'){
|
|
167
|
-
return result;
|
|
173
|
+
return withoutMailCredentials(result);
|
|
168
174
|
}
|
|
169
|
-
const details=
|
|
175
|
+
const details=completeSendFailure(result);
|
|
170
176
|
throw new ArcaneError(
|
|
171
177
|
ERROR_CODES.operationFailed,
|
|
172
178
|
`Resend did not authoritatively accept the mail request (${details.classification||'unknown'}).`,
|
package/src/packager/core.mjs
CHANGED
|
@@ -499,16 +499,23 @@ async function collectPackageRecords(context,{signal}={}){
|
|
|
499
499
|
return records;
|
|
500
500
|
}
|
|
501
501
|
|
|
502
|
-
async function browserDocuments(records){
|
|
502
|
+
async function browserDocuments(records,entry){
|
|
503
|
+
let entryDocument=null;
|
|
503
504
|
const documents=[];
|
|
504
505
|
for(const record of records){
|
|
505
|
-
|
|
506
|
+
const extension=path.posix.extname(record.destination).toLocaleLowerCase('en-US');
|
|
507
|
+
if(extension!=='.html'&&extension!=='.htm')continue;
|
|
506
508
|
const inspected=inspectImportMapHtml(await readFile(record.source,'utf8'),{
|
|
507
509
|
documentPath:record.destination
|
|
508
510
|
});
|
|
509
|
-
|
|
511
|
+
const document={path:record.destination,...copyJson(inspected)};
|
|
512
|
+
if(record.destination===entry){
|
|
513
|
+
entryDocument=document;
|
|
514
|
+
}else if(inspected.bases.length>0){
|
|
515
|
+
documents.push(document);
|
|
516
|
+
}
|
|
510
517
|
}
|
|
511
|
-
return documents;
|
|
518
|
+
return entryDocument===null?documents:[entryDocument,...documents];
|
|
512
519
|
}
|
|
513
520
|
|
|
514
521
|
async function optionalDescriptor(context){
|
|
@@ -540,7 +547,7 @@ async function inspectContext(context,{signal}={}){
|
|
|
540
547
|
}),
|
|
541
548
|
...(context.config.adapter===undefined?{}:{adapter:context.config.adapter}),
|
|
542
549
|
descriptor:await optionalDescriptor(context),
|
|
543
|
-
browserDocuments:await browserDocuments(records),
|
|
550
|
+
browserDocuments:await browserDocuments(records,context.config.entry),
|
|
544
551
|
files:records.map(record=>record.destination),
|
|
545
552
|
output:path.relative(context.workspaceRoot,context.outputRoot).split(path.sep).join('/')
|
|
546
553
|
};
|
package/src/scaffold.mjs
CHANGED
|
@@ -430,6 +430,25 @@ export async function initWorkspace({
|
|
|
430
430
|
sdkDeclaration
|
|
431
431
|
);
|
|
432
432
|
const writtenFiles=await writeMissingFiles(resolvedRoot,template.files,{signal,onEvent});
|
|
433
|
+
if(workspaceMode==='external'&&writtenFiles.skippedFiles.includes('arcane.lock.json')){
|
|
434
|
+
throwIfAborted(signal);
|
|
435
|
+
await assertNoLinkedAncestors(resolvedRoot,'arcane.lock.json');
|
|
436
|
+
const lockPath=path.join(resolvedRoot,'arcane.lock.json');
|
|
437
|
+
const lockInfo=await lstat(lockPath);
|
|
438
|
+
if(lockInfo.isSymbolicLink()||!lockInfo.isFile()){
|
|
439
|
+
fail('Existing arcane.lock.json must be a real file.');
|
|
440
|
+
}
|
|
441
|
+
await writeFile(
|
|
442
|
+
lockPath,
|
|
443
|
+
template.files.get('arcane.lock.json'),
|
|
444
|
+
'utf8'
|
|
445
|
+
);
|
|
446
|
+
writtenFiles.skippedFiles=writtenFiles.skippedFiles.filter(
|
|
447
|
+
relative=>relative!=='arcane.lock.json'
|
|
448
|
+
);
|
|
449
|
+
writtenFiles.replacedFiles=['arcane.lock.json'];
|
|
450
|
+
await emit(onEvent,{type:'scaffold.file.replaced',path:'arcane.lock.json'});
|
|
451
|
+
}
|
|
433
452
|
const packageUpdated=await applyPackageMerge(resolvedRoot,packagePlan,{signal,onEvent});
|
|
434
453
|
const workspaceRuntime=workspaceMode==='external'
|
|
435
454
|
?await materializeWorkspaceRuntimeContent({
|
|
@@ -1,4 +1,10 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
ARCANE_PROTOCOL,
|
|
3
|
+
CLI_EVENT_PROTOCOL,
|
|
4
|
+
SDK_NAME,
|
|
5
|
+
SDK_VERSION,
|
|
6
|
+
TARGET_ADAPTER_PROTOCOL
|
|
7
|
+
} from '../constants.mjs';
|
|
2
8
|
|
|
3
9
|
const NPM_PACKAGE_NAME_PATTERN=/^(?:@[a-z0-9][a-z0-9._-]*\/)?[a-z0-9][a-z0-9._-]*$/u;
|
|
4
10
|
const LOCAL_TARBALL_PATTERN=/^file:.+\.tgz$/iu;
|
|
@@ -22,6 +28,32 @@ function titleCase(appId){
|
|
|
22
28
|
.join(' ');
|
|
23
29
|
}
|
|
24
30
|
|
|
31
|
+
export function createWorkspaceLockDocument({
|
|
32
|
+
dependencyName=SDK_NAME,
|
|
33
|
+
packageName=SDK_NAME,
|
|
34
|
+
packageVersion=SDK_VERSION,
|
|
35
|
+
packageSource=`node_modules/${dependencyName}`
|
|
36
|
+
}={}){
|
|
37
|
+
if(packageName!==SDK_NAME||typeof packageVersion!=='string'
|
|
38
|
+
||!/^[0-9]+\.[0-9]+\.[0-9]+(?:-[0-9A-Za-z.-]+)?$/u.test(packageVersion)
|
|
39
|
+
||typeof dependencyName!=='string'||dependencyName.length>214
|
|
40
|
+
||!NPM_PACKAGE_NAME_PATTERN.test(dependencyName)
|
|
41
|
+
||packageSource!==`node_modules/${dependencyName}`){
|
|
42
|
+
throw new Error('Invalid workspace SDK lock authority.');
|
|
43
|
+
}
|
|
44
|
+
return {
|
|
45
|
+
schemaVersion:1,
|
|
46
|
+
sdk:{name:packageName,version:packageVersion},
|
|
47
|
+
runtime:{root:`${packageSource}/runtime`},
|
|
48
|
+
sdkBrowserRuntime:{root:`${packageSource}/browser-runtime`},
|
|
49
|
+
protocols:{
|
|
50
|
+
arcane:ARCANE_PROTOCOL,
|
|
51
|
+
cliEvents:CLI_EVENT_PROTOCOL,
|
|
52
|
+
targetAdapter:TARGET_ADAPTER_PROTOCOL
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
25
57
|
export function workspaceTemplate({
|
|
26
58
|
appId,
|
|
27
59
|
displayName,
|
|
@@ -212,6 +244,10 @@ Every browser release also carries Arcane OS licensing material under \`licenses
|
|
|
212
244
|
]
|
|
213
245
|
}
|
|
214
246
|
}));
|
|
247
|
+
files.set('arcane.lock.json',json(createWorkspaceLockDocument({
|
|
248
|
+
dependencyName:sdkDependencyName,
|
|
249
|
+
packageSource:sdkPackageSource
|
|
250
|
+
})));
|
|
215
251
|
files.set(`apps/${appId}/arcane-app.json`,json({
|
|
216
252
|
schemaVersion:2,
|
|
217
253
|
id:appId,
|
package/src/toolchain.mjs
CHANGED
|
@@ -314,7 +314,9 @@ async function refreshPreparedImportMap(prepared,{signal,onEvent,workspaceOperat
|
|
|
314
314
|
appId:prepared.appId,
|
|
315
315
|
appRoot:prepared.appRoot,
|
|
316
316
|
entry:manifest.entry,
|
|
317
|
-
documents:inspected.browserDocuments
|
|
317
|
+
documents:inspected.browserDocuments.map(function selectBrowserDocumentPath(document){
|
|
318
|
+
return document.path;
|
|
319
|
+
}),
|
|
318
320
|
workspaceOperationLease,
|
|
319
321
|
signal,
|
|
320
322
|
onEvent
|
package/src/workspace.mjs
CHANGED
|
@@ -94,6 +94,18 @@ function dependencyNameForSdkPackageSource(source){
|
|
|
94
94
|
}
|
|
95
95
|
}
|
|
96
96
|
|
|
97
|
+
function routeIncludeMatches(actual,wanted,optionalSecurity){
|
|
98
|
+
if(!Array.isArray(actual)||!Array.isArray(wanted))return false;
|
|
99
|
+
if(actual.length===wanted.length
|
|
100
|
+
&&actual.every(function sameIncludedPath(value,index){return value===wanted[index];})){
|
|
101
|
+
return true;
|
|
102
|
+
}
|
|
103
|
+
return optionalSecurity
|
|
104
|
+
&&wanted.at(-1)==='security'
|
|
105
|
+
&&actual.length===wanted.length-1
|
|
106
|
+
&&actual.every(function sameFunctionalPath(value,index){return value===wanted[index];});
|
|
107
|
+
}
|
|
108
|
+
|
|
97
109
|
export function resolveSdkPackageDeclaration(rootPackage,{
|
|
98
110
|
allowMissing=false,
|
|
99
111
|
packageSource
|
|
@@ -179,19 +191,30 @@ function classifyRootConfig(config){
|
|
|
179
191
|
include:['index.js','licence','package.json']
|
|
180
192
|
}
|
|
181
193
|
];
|
|
182
|
-
const matches=expected
|
|
183
|
-
|
|
184
|
-
return
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
194
|
+
const matches=(expected,{optionalArcaneSecurity=false}={})=>{
|
|
195
|
+
if(routes.length!==expected.length)return false;
|
|
196
|
+
return routes.every(function routeMatches(route,index){
|
|
197
|
+
const wanted=expected[index];
|
|
198
|
+
return isObject(route)
|
|
199
|
+
&&Object.keys(route).every(function knownRouteKey(key){
|
|
200
|
+
return ['source','destination','include','exclude'].includes(key);
|
|
201
|
+
})
|
|
202
|
+
&&route.source===wanted.source&&route.destination===wanted.destination
|
|
203
|
+
&&routeIncludeMatches(
|
|
204
|
+
route.include,
|
|
205
|
+
wanted.include,
|
|
206
|
+
optionalArcaneSecurity&&index===0
|
|
207
|
+
&&wanted.source==='arcane'&&wanted.destination==='arcane'
|
|
208
|
+
)
|
|
209
|
+
&&Array.isArray(route.exclude)&&route.exclude.length===0;
|
|
210
|
+
});
|
|
211
|
+
};
|
|
189
212
|
let workspaceMode;
|
|
190
213
|
let browserRuntimeLayout;
|
|
191
|
-
if(matches(external)){
|
|
214
|
+
if(matches(external,{optionalArcaneSecurity:true})){
|
|
192
215
|
workspaceMode='external';
|
|
193
216
|
browserRuntimeLayout='physical-v1';
|
|
194
|
-
}else if(matches(integrated)){
|
|
217
|
+
}else if(matches(integrated,{optionalArcaneSecurity:true})){
|
|
195
218
|
workspaceMode='integrated';
|
|
196
219
|
browserRuntimeLayout='physical-v1';
|
|
197
220
|
}else if(matches(integratedLegacy)){
|