@arcote.tech/arc-cli 0.4.5 → 0.4.7
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/dist/index.js +266 -104
- package/package.json +1 -1
- package/src/builder/module-builder.ts +32 -5
- package/src/commands/platform-dev.ts +2 -1
- package/src/commands/platform-start.ts +2 -1
- package/src/i18n/catalog.ts +43 -5
- package/src/i18n/compile.ts +6 -1
- package/src/platform/server.ts +97 -10
- package/src/platform/shared.ts +67 -28
package/dist/index.js
CHANGED
|
@@ -9353,7 +9353,7 @@ var require_buffer_equal_constant_time = __commonJS((exports, module) => {
|
|
|
9353
9353
|
// ../../node_modules/jwa/index.js
|
|
9354
9354
|
var require_jwa = __commonJS((exports, module) => {
|
|
9355
9355
|
var Buffer2 = require_safe_buffer().Buffer;
|
|
9356
|
-
var
|
|
9356
|
+
var crypto2 = __require("node:crypto");
|
|
9357
9357
|
var formatEcdsa = require_ecdsa_sig_formatter();
|
|
9358
9358
|
var util = __require("node:util");
|
|
9359
9359
|
var MSG_INVALID_ALGORITHM = `"%s" is not a valid algorithm.
|
|
@@ -9362,7 +9362,7 @@ var require_jwa = __commonJS((exports, module) => {
|
|
|
9362
9362
|
var MSG_INVALID_SECRET = "secret must be a string or buffer";
|
|
9363
9363
|
var MSG_INVALID_VERIFIER_KEY = "key must be a string or a buffer";
|
|
9364
9364
|
var MSG_INVALID_SIGNER_KEY = "key must be a string, a buffer or an object";
|
|
9365
|
-
var supportsKeyObjects = typeof
|
|
9365
|
+
var supportsKeyObjects = typeof crypto2.createPublicKey === "function";
|
|
9366
9366
|
if (supportsKeyObjects) {
|
|
9367
9367
|
MSG_INVALID_VERIFIER_KEY += " or a KeyObject";
|
|
9368
9368
|
MSG_INVALID_SECRET += "or a KeyObject";
|
|
@@ -9452,17 +9452,17 @@ var require_jwa = __commonJS((exports, module) => {
|
|
|
9452
9452
|
return function sign(thing, secret) {
|
|
9453
9453
|
checkIsSecretKey(secret);
|
|
9454
9454
|
thing = normalizeInput(thing);
|
|
9455
|
-
var hmac =
|
|
9455
|
+
var hmac = crypto2.createHmac("sha" + bits, secret);
|
|
9456
9456
|
var sig = (hmac.update(thing), hmac.digest("base64"));
|
|
9457
9457
|
return fromBase64(sig);
|
|
9458
9458
|
};
|
|
9459
9459
|
}
|
|
9460
9460
|
var bufferEqual;
|
|
9461
|
-
var timingSafeEqual = "timingSafeEqual" in
|
|
9461
|
+
var timingSafeEqual = "timingSafeEqual" in crypto2 ? function timingSafeEqual(a, b) {
|
|
9462
9462
|
if (a.byteLength !== b.byteLength) {
|
|
9463
9463
|
return false;
|
|
9464
9464
|
}
|
|
9465
|
-
return
|
|
9465
|
+
return crypto2.timingSafeEqual(a, b);
|
|
9466
9466
|
} : function timingSafeEqual(a, b) {
|
|
9467
9467
|
if (!bufferEqual) {
|
|
9468
9468
|
bufferEqual = require_buffer_equal_constant_time();
|
|
@@ -9479,7 +9479,7 @@ var require_jwa = __commonJS((exports, module) => {
|
|
|
9479
9479
|
return function sign(thing, privateKey) {
|
|
9480
9480
|
checkIsPrivateKey(privateKey);
|
|
9481
9481
|
thing = normalizeInput(thing);
|
|
9482
|
-
var signer =
|
|
9482
|
+
var signer = crypto2.createSign("RSA-SHA" + bits);
|
|
9483
9483
|
var sig = (signer.update(thing), signer.sign(privateKey, "base64"));
|
|
9484
9484
|
return fromBase64(sig);
|
|
9485
9485
|
};
|
|
@@ -9489,7 +9489,7 @@ var require_jwa = __commonJS((exports, module) => {
|
|
|
9489
9489
|
checkIsPublicKey(publicKey);
|
|
9490
9490
|
thing = normalizeInput(thing);
|
|
9491
9491
|
signature = toBase64(signature);
|
|
9492
|
-
var verifier =
|
|
9492
|
+
var verifier = crypto2.createVerify("RSA-SHA" + bits);
|
|
9493
9493
|
verifier.update(thing);
|
|
9494
9494
|
return verifier.verify(publicKey, signature, "base64");
|
|
9495
9495
|
};
|
|
@@ -9498,11 +9498,11 @@ var require_jwa = __commonJS((exports, module) => {
|
|
|
9498
9498
|
return function sign(thing, privateKey) {
|
|
9499
9499
|
checkIsPrivateKey(privateKey);
|
|
9500
9500
|
thing = normalizeInput(thing);
|
|
9501
|
-
var signer =
|
|
9501
|
+
var signer = crypto2.createSign("RSA-SHA" + bits);
|
|
9502
9502
|
var sig = (signer.update(thing), signer.sign({
|
|
9503
9503
|
key: privateKey,
|
|
9504
|
-
padding:
|
|
9505
|
-
saltLength:
|
|
9504
|
+
padding: crypto2.constants.RSA_PKCS1_PSS_PADDING,
|
|
9505
|
+
saltLength: crypto2.constants.RSA_PSS_SALTLEN_DIGEST
|
|
9506
9506
|
}, "base64"));
|
|
9507
9507
|
return fromBase64(sig);
|
|
9508
9508
|
};
|
|
@@ -9512,12 +9512,12 @@ var require_jwa = __commonJS((exports, module) => {
|
|
|
9512
9512
|
checkIsPublicKey(publicKey);
|
|
9513
9513
|
thing = normalizeInput(thing);
|
|
9514
9514
|
signature = toBase64(signature);
|
|
9515
|
-
var verifier =
|
|
9515
|
+
var verifier = crypto2.createVerify("RSA-SHA" + bits);
|
|
9516
9516
|
verifier.update(thing);
|
|
9517
9517
|
return verifier.verify({
|
|
9518
9518
|
key: publicKey,
|
|
9519
|
-
padding:
|
|
9520
|
-
saltLength:
|
|
9519
|
+
padding: crypto2.constants.RSA_PKCS1_PSS_PADDING,
|
|
9520
|
+
saltLength: crypto2.constants.RSA_PSS_SALTLEN_DIGEST
|
|
9521
9521
|
}, signature, "base64");
|
|
9522
9522
|
};
|
|
9523
9523
|
}
|
|
@@ -14340,15 +14340,15 @@ function typeValidatorBuilder(typeName, comparatorStrategy) {
|
|
|
14340
14340
|
function string() {
|
|
14341
14341
|
return new ArcString;
|
|
14342
14342
|
}
|
|
14343
|
-
function id(name, generateFn) {
|
|
14344
|
-
return new ArcId(name, generateFn);
|
|
14345
|
-
}
|
|
14346
14343
|
|
|
14347
14344
|
class ArcFragmentBase {
|
|
14348
14345
|
is(type) {
|
|
14349
14346
|
return this.types.includes(type);
|
|
14350
14347
|
}
|
|
14351
14348
|
}
|
|
14349
|
+
function id(name, generateFn) {
|
|
14350
|
+
return new ArcId(name, generateFn);
|
|
14351
|
+
}
|
|
14352
14352
|
|
|
14353
14353
|
class AggregateBase {
|
|
14354
14354
|
value;
|
|
@@ -15957,7 +15957,7 @@ var Operation, PROXY_DRAFT, RAW_RETURN_SYMBOL, iteratorSymbol, dataTypes, intern
|
|
|
15957
15957
|
}
|
|
15958
15958
|
return returnValue(result);
|
|
15959
15959
|
};
|
|
15960
|
-
}, create, constructorString, TOKEN_PREFIX = "arc:token:", eventWireInstanceCounter = 0, EVENT_TABLES, arrayValidator, ArcArray, objectValidator, ArcObject, ArcPrimitive,
|
|
15960
|
+
}, create, constructorString, TOKEN_PREFIX = "arc:token:", eventWireInstanceCounter = 0, EVENT_TABLES, arrayValidator, ArcArray, objectValidator, ArcObject, ArcPrimitive, stringValidator, ArcString, ArcContextElement, ArcBoolean, ArcId, numberValidator, ArcNumber, ArcEvent, ForkedStoreState, ForkedDataStorage, MasterStoreState, MasterDataStorage2, dateValidator, SQLiteReadWriteTransaction, createSQLiteAdapterFactory = (db) => {
|
|
15961
15961
|
return async (context) => {
|
|
15962
15962
|
const adapter = new SQLiteAdapter(db, context);
|
|
15963
15963
|
await adapter.initialize();
|
|
@@ -16648,37 +16648,6 @@ var init_dist = __esm(() => {
|
|
|
16648
16648
|
return value;
|
|
16649
16649
|
}
|
|
16650
16650
|
};
|
|
16651
|
-
ArcBoolean = class ArcBoolean extends ArcPrimitive {
|
|
16652
|
-
hasToBeTrue() {
|
|
16653
|
-
return this.validation("hasToBeTrue", (value) => {
|
|
16654
|
-
if (!value)
|
|
16655
|
-
return {
|
|
16656
|
-
current: value
|
|
16657
|
-
};
|
|
16658
|
-
});
|
|
16659
|
-
}
|
|
16660
|
-
validation(name, validator) {
|
|
16661
|
-
const instance = this.pipeValidation(name, validator);
|
|
16662
|
-
return instance;
|
|
16663
|
-
}
|
|
16664
|
-
toJsonSchema() {
|
|
16665
|
-
const schema = { type: "boolean" };
|
|
16666
|
-
if (this._description) {
|
|
16667
|
-
schema.description = this._description;
|
|
16668
|
-
}
|
|
16669
|
-
return schema;
|
|
16670
|
-
}
|
|
16671
|
-
getColumnData() {
|
|
16672
|
-
const storeData = this.getStoreData();
|
|
16673
|
-
return {
|
|
16674
|
-
type: "boolean",
|
|
16675
|
-
storeData: {
|
|
16676
|
-
...storeData,
|
|
16677
|
-
isNullable: false
|
|
16678
|
-
}
|
|
16679
|
-
};
|
|
16680
|
-
}
|
|
16681
|
-
};
|
|
16682
16651
|
stringValidator = typeValidatorBuilder("string");
|
|
16683
16652
|
ArcString = class ArcString extends ArcPrimitive {
|
|
16684
16653
|
constructor() {
|
|
@@ -16826,6 +16795,52 @@ var init_dist = __esm(() => {
|
|
|
16826
16795
|
};
|
|
16827
16796
|
}
|
|
16828
16797
|
};
|
|
16798
|
+
ArcContextElement = class ArcContextElement extends ArcFragmentBase {
|
|
16799
|
+
name;
|
|
16800
|
+
types = ["context-element"];
|
|
16801
|
+
get id() {
|
|
16802
|
+
return this.name;
|
|
16803
|
+
}
|
|
16804
|
+
constructor(name) {
|
|
16805
|
+
super();
|
|
16806
|
+
this.name = name;
|
|
16807
|
+
}
|
|
16808
|
+
_seeds;
|
|
16809
|
+
getSeeds() {
|
|
16810
|
+
return this._seeds;
|
|
16811
|
+
}
|
|
16812
|
+
};
|
|
16813
|
+
ArcBoolean = class ArcBoolean extends ArcPrimitive {
|
|
16814
|
+
hasToBeTrue() {
|
|
16815
|
+
return this.validation("hasToBeTrue", (value) => {
|
|
16816
|
+
if (!value)
|
|
16817
|
+
return {
|
|
16818
|
+
current: value
|
|
16819
|
+
};
|
|
16820
|
+
});
|
|
16821
|
+
}
|
|
16822
|
+
validation(name, validator) {
|
|
16823
|
+
const instance = this.pipeValidation(name, validator);
|
|
16824
|
+
return instance;
|
|
16825
|
+
}
|
|
16826
|
+
toJsonSchema() {
|
|
16827
|
+
const schema = { type: "boolean" };
|
|
16828
|
+
if (this._description) {
|
|
16829
|
+
schema.description = this._description;
|
|
16830
|
+
}
|
|
16831
|
+
return schema;
|
|
16832
|
+
}
|
|
16833
|
+
getColumnData() {
|
|
16834
|
+
const storeData = this.getStoreData();
|
|
16835
|
+
return {
|
|
16836
|
+
type: "boolean",
|
|
16837
|
+
storeData: {
|
|
16838
|
+
...storeData,
|
|
16839
|
+
isNullable: false
|
|
16840
|
+
}
|
|
16841
|
+
};
|
|
16842
|
+
}
|
|
16843
|
+
};
|
|
16829
16844
|
ArcId = class ArcId extends ArcBranded {
|
|
16830
16845
|
generateFn;
|
|
16831
16846
|
constructor(name, generateFn) {
|
|
@@ -16891,17 +16906,6 @@ var init_dist = __esm(() => {
|
|
|
16891
16906
|
};
|
|
16892
16907
|
}
|
|
16893
16908
|
};
|
|
16894
|
-
ArcContextElement = class ArcContextElement extends ArcFragmentBase {
|
|
16895
|
-
name;
|
|
16896
|
-
types = ["context-element"];
|
|
16897
|
-
get id() {
|
|
16898
|
-
return this.name;
|
|
16899
|
-
}
|
|
16900
|
-
constructor(name) {
|
|
16901
|
-
super();
|
|
16902
|
-
this.name = name;
|
|
16903
|
-
}
|
|
16904
|
-
};
|
|
16905
16909
|
ArcEvent = class ArcEvent extends ArcContextElement {
|
|
16906
16910
|
data;
|
|
16907
16911
|
eventId = id("event");
|
|
@@ -25541,6 +25545,7 @@ function parsePo(content) {
|
|
|
25541
25545
|
let msgid = "";
|
|
25542
25546
|
let msgstr = "";
|
|
25543
25547
|
let obsolete = false;
|
|
25548
|
+
let inManual = false;
|
|
25544
25549
|
const flush = () => {
|
|
25545
25550
|
if (msgid) {
|
|
25546
25551
|
entries.push({
|
|
@@ -25548,7 +25553,8 @@ function parsePo(content) {
|
|
|
25548
25553
|
msgstr,
|
|
25549
25554
|
locations,
|
|
25550
25555
|
hash: hash || hashMsgid(msgid),
|
|
25551
|
-
obsolete
|
|
25556
|
+
obsolete,
|
|
25557
|
+
...inManual ? { manual: true } : {}
|
|
25552
25558
|
});
|
|
25553
25559
|
}
|
|
25554
25560
|
locations = [];
|
|
@@ -25559,6 +25565,16 @@ function parsePo(content) {
|
|
|
25559
25565
|
};
|
|
25560
25566
|
for (const line of lines) {
|
|
25561
25567
|
const trimmed = line.trim();
|
|
25568
|
+
if (trimmed === "# manual") {
|
|
25569
|
+
inManual = true;
|
|
25570
|
+
continue;
|
|
25571
|
+
}
|
|
25572
|
+
if (trimmed === "# end manual") {
|
|
25573
|
+
if (msgid)
|
|
25574
|
+
flush();
|
|
25575
|
+
inManual = false;
|
|
25576
|
+
continue;
|
|
25577
|
+
}
|
|
25562
25578
|
if (trimmed === "" || trimmed.startsWith("#,")) {
|
|
25563
25579
|
if (msgid)
|
|
25564
25580
|
flush();
|
|
@@ -25601,8 +25617,19 @@ function parsePo(content) {
|
|
|
25601
25617
|
}
|
|
25602
25618
|
function writePo(entries) {
|
|
25603
25619
|
const lines = [];
|
|
25604
|
-
const
|
|
25620
|
+
const manual = entries.filter((e) => e.manual);
|
|
25621
|
+
const active = entries.filter((e) => !e.obsolete && !e.manual);
|
|
25605
25622
|
const obsolete = entries.filter((e) => e.obsolete);
|
|
25623
|
+
if (manual.length > 0) {
|
|
25624
|
+
lines.push("# manual");
|
|
25625
|
+
for (const entry of manual) {
|
|
25626
|
+
lines.push(`msgid ${quoteString(entry.msgid)}`);
|
|
25627
|
+
lines.push(`msgstr ${quoteString(entry.msgstr)}`);
|
|
25628
|
+
lines.push("");
|
|
25629
|
+
}
|
|
25630
|
+
lines.push("# end manual");
|
|
25631
|
+
lines.push("");
|
|
25632
|
+
}
|
|
25606
25633
|
for (const entry of active) {
|
|
25607
25634
|
for (const loc of entry.locations) {
|
|
25608
25635
|
lines.push(`#: ${loc}`);
|
|
@@ -25629,10 +25656,14 @@ function mergeCatalog(existing, extracted) {
|
|
|
25629
25656
|
for (const entry of existing) {
|
|
25630
25657
|
existingMap.set(entry.msgid, entry);
|
|
25631
25658
|
}
|
|
25659
|
+
const manualEntries = existing.filter((e) => e.manual);
|
|
25660
|
+
const manualIds = new Set(manualEntries.map((e) => e.msgid));
|
|
25632
25661
|
const result = [];
|
|
25633
25662
|
const seen = new Set;
|
|
25634
25663
|
for (const [msgid, locations] of extracted) {
|
|
25635
25664
|
seen.add(msgid);
|
|
25665
|
+
if (manualIds.has(msgid))
|
|
25666
|
+
continue;
|
|
25636
25667
|
const prev = existingMap.get(msgid);
|
|
25637
25668
|
result.push({
|
|
25638
25669
|
msgid,
|
|
@@ -25643,7 +25674,7 @@ function mergeCatalog(existing, extracted) {
|
|
|
25643
25674
|
});
|
|
25644
25675
|
}
|
|
25645
25676
|
for (const entry of existing) {
|
|
25646
|
-
if (!seen.has(entry.msgid) && !entry.obsolete && entry.msgstr) {
|
|
25677
|
+
if (!seen.has(entry.msgid) && !entry.obsolete && !entry.manual && entry.msgstr) {
|
|
25647
25678
|
result.push({
|
|
25648
25679
|
...entry,
|
|
25649
25680
|
obsolete: true,
|
|
@@ -25651,7 +25682,8 @@ function mergeCatalog(existing, extracted) {
|
|
|
25651
25682
|
});
|
|
25652
25683
|
}
|
|
25653
25684
|
}
|
|
25654
|
-
|
|
25685
|
+
result.sort((a, b) => a.msgid.localeCompare(b.msgid));
|
|
25686
|
+
return [...manualEntries, ...result];
|
|
25655
25687
|
}
|
|
25656
25688
|
function extractQuoted(s) {
|
|
25657
25689
|
const trimmed = s.trim();
|
|
@@ -25678,7 +25710,11 @@ function compileCatalog(poPath) {
|
|
|
25678
25710
|
result[entry.msgid] = entry.msgstr;
|
|
25679
25711
|
}
|
|
25680
25712
|
}
|
|
25681
|
-
|
|
25713
|
+
const sorted = {};
|
|
25714
|
+
for (const key of Object.keys(result).sort()) {
|
|
25715
|
+
sorted[key] = result[key];
|
|
25716
|
+
}
|
|
25717
|
+
return sorted;
|
|
25682
25718
|
}
|
|
25683
25719
|
function compileAllCatalogs(localesDir, outDir) {
|
|
25684
25720
|
mkdirSync3(outDir, { recursive: true });
|
|
@@ -25806,6 +25842,9 @@ var SHELL_EXTERNALS = [
|
|
|
25806
25842
|
"@arcote.tech/arc",
|
|
25807
25843
|
"@arcote.tech/arc-ds",
|
|
25808
25844
|
"@arcote.tech/arc-react",
|
|
25845
|
+
"@arcote.tech/arc-auth",
|
|
25846
|
+
"@arcote.tech/arc-utils",
|
|
25847
|
+
"@arcote.tech/arc-workspace",
|
|
25809
25848
|
"@arcote.tech/platform"
|
|
25810
25849
|
];
|
|
25811
25850
|
function discoverPackages(rootDir) {
|
|
@@ -25881,8 +25920,11 @@ async function buildPackages(rootDir, outDir, packages) {
|
|
|
25881
25920
|
const tmpDir = join6(outDir, "_entries");
|
|
25882
25921
|
mkdirSync5(tmpDir, { recursive: true });
|
|
25883
25922
|
const entrypoints = [];
|
|
25923
|
+
const fileToName = new Map;
|
|
25884
25924
|
for (const pkg of packages) {
|
|
25885
25925
|
const safeName = pkg.path.split("/").pop();
|
|
25926
|
+
const moduleName = pkg.name.includes("/") ? pkg.name.split("/").pop() : pkg.name;
|
|
25927
|
+
fileToName.set(safeName, moduleName);
|
|
25886
25928
|
const wrapperFile = join6(tmpDir, `${safeName}.ts`);
|
|
25887
25929
|
writeFileSync5(wrapperFile, `export * from "${pkg.name}";
|
|
25888
25930
|
`);
|
|
@@ -25890,6 +25932,14 @@ async function buildPackages(rootDir, outDir, packages) {
|
|
|
25890
25932
|
}
|
|
25891
25933
|
console.log(` Bundling ${entrypoints.length} package(s)...`);
|
|
25892
25934
|
const i18nCollector = new Map;
|
|
25935
|
+
const arcExternalPlugin = {
|
|
25936
|
+
name: "arc-external",
|
|
25937
|
+
setup(build2) {
|
|
25938
|
+
build2.onResolve({ filter: /^@arcote\.tech\// }, (args) => {
|
|
25939
|
+
return { path: args.path, external: true };
|
|
25940
|
+
});
|
|
25941
|
+
}
|
|
25942
|
+
};
|
|
25893
25943
|
const result = await Bun.build({
|
|
25894
25944
|
entrypoints,
|
|
25895
25945
|
outdir: outDir,
|
|
@@ -25897,7 +25947,7 @@ async function buildPackages(rootDir, outDir, packages) {
|
|
|
25897
25947
|
format: "esm",
|
|
25898
25948
|
target: "browser",
|
|
25899
25949
|
external: SHELL_EXTERNALS,
|
|
25900
|
-
plugins: [i18nExtractPlugin(i18nCollector, rootDir)],
|
|
25950
|
+
plugins: [arcExternalPlugin, i18nExtractPlugin(i18nCollector, rootDir)],
|
|
25901
25951
|
naming: "[name].[ext]",
|
|
25902
25952
|
define: {
|
|
25903
25953
|
ONLY_SERVER: "false",
|
|
@@ -25914,9 +25964,13 @@ async function buildPackages(rootDir, outDir, packages) {
|
|
|
25914
25964
|
await finalizeTranslations(rootDir, join6(outDir, ".."), i18nCollector);
|
|
25915
25965
|
const { rmSync } = await import("node:fs");
|
|
25916
25966
|
rmSync(tmpDir, { recursive: true, force: true });
|
|
25917
|
-
const
|
|
25967
|
+
const moduleEntries = result.outputs.filter((o) => o.kind === "entry-point").map((o) => {
|
|
25968
|
+
const file = o.path.split("/").pop();
|
|
25969
|
+
const safeName = file.replace(/\.js$/, "");
|
|
25970
|
+
return { file, name: fileToName.get(safeName) ?? safeName };
|
|
25971
|
+
});
|
|
25918
25972
|
const manifest = {
|
|
25919
|
-
modules:
|
|
25973
|
+
modules: moduleEntries,
|
|
25920
25974
|
buildTime: new Date().toISOString()
|
|
25921
25975
|
};
|
|
25922
25976
|
writeFileSync5(join6(outDir, "manifest.json"), JSON.stringify(manifest, null, 2));
|
|
@@ -26128,34 +26182,44 @@ export const { createPortal, flushSync } = ReactDOM;`
|
|
|
26128
26182
|
["arc", "@arcote.tech/arc"],
|
|
26129
26183
|
["arc-ds", "@arcote.tech/arc-ds"],
|
|
26130
26184
|
["arc-react", "@arcote.tech/arc-react"],
|
|
26185
|
+
["arc-auth", "@arcote.tech/arc-auth"],
|
|
26186
|
+
["arc-utils", "@arcote.tech/arc-utils"],
|
|
26187
|
+
["arc-workspace", "@arcote.tech/arc-workspace"],
|
|
26131
26188
|
["platform", "@arcote.tech/platform"]
|
|
26132
26189
|
];
|
|
26133
|
-
const
|
|
26190
|
+
const baseExternal = [
|
|
26191
|
+
"react",
|
|
26192
|
+
"react-dom",
|
|
26193
|
+
"react/jsx-runtime",
|
|
26194
|
+
"react/jsx-dev-runtime",
|
|
26195
|
+
"react-dom/client"
|
|
26196
|
+
];
|
|
26197
|
+
const allArcPkgs = arcEntries.map(([, pkg]) => pkg);
|
|
26134
26198
|
for (const [name, pkg] of arcEntries) {
|
|
26135
26199
|
const f = join7(tmpDir, `${name}.ts`);
|
|
26136
26200
|
Bun.write(f, `export * from "${pkg}";
|
|
26137
26201
|
`);
|
|
26138
|
-
|
|
26139
|
-
|
|
26140
|
-
|
|
26141
|
-
|
|
26142
|
-
|
|
26143
|
-
|
|
26144
|
-
|
|
26145
|
-
|
|
26146
|
-
|
|
26147
|
-
|
|
26148
|
-
|
|
26149
|
-
|
|
26150
|
-
|
|
26151
|
-
|
|
26152
|
-
|
|
26153
|
-
|
|
26154
|
-
|
|
26155
|
-
|
|
26156
|
-
|
|
26157
|
-
|
|
26158
|
-
|
|
26202
|
+
const r2 = await Bun.build({
|
|
26203
|
+
entrypoints: [f],
|
|
26204
|
+
outdir: outDir,
|
|
26205
|
+
format: "esm",
|
|
26206
|
+
target: "browser",
|
|
26207
|
+
naming: "[name].[ext]",
|
|
26208
|
+
external: [
|
|
26209
|
+
...baseExternal,
|
|
26210
|
+
...allArcPkgs.filter((p) => p !== pkg)
|
|
26211
|
+
],
|
|
26212
|
+
define: {
|
|
26213
|
+
ONLY_SERVER: "false",
|
|
26214
|
+
ONLY_BROWSER: "true",
|
|
26215
|
+
ONLY_CLIENT: "true"
|
|
26216
|
+
}
|
|
26217
|
+
});
|
|
26218
|
+
if (!r2.success) {
|
|
26219
|
+
for (const l of r2.logs)
|
|
26220
|
+
console.error(l);
|
|
26221
|
+
throw new Error(`Shell build failed for ${pkg}`);
|
|
26222
|
+
}
|
|
26159
26223
|
}
|
|
26160
26224
|
const { rmSync } = await import("node:fs");
|
|
26161
26225
|
rmSync(tmpDir, { recursive: true, force: true });
|
|
@@ -26163,10 +26227,14 @@ export const { createPortal, flushSync } = ReactDOM;`
|
|
|
26163
26227
|
async function loadServerContext(packages) {
|
|
26164
26228
|
const ctxPackages = packages.filter((p) => isContextPackage(p.packageJson));
|
|
26165
26229
|
if (ctxPackages.length === 0)
|
|
26166
|
-
return null;
|
|
26230
|
+
return { context: null, moduleAccess: new Map };
|
|
26167
26231
|
globalThis.ONLY_SERVER = true;
|
|
26168
26232
|
globalThis.ONLY_BROWSER = false;
|
|
26169
26233
|
globalThis.ONLY_CLIENT = false;
|
|
26234
|
+
const platformDir = join7(process.cwd(), "node_modules", "@arcote.tech", "platform");
|
|
26235
|
+
const platformPkg = JSON.parse(readFileSync6(join7(platformDir, "package.json"), "utf-8"));
|
|
26236
|
+
const platformEntry = join7(platformDir, platformPkg.main ?? "src/index.ts");
|
|
26237
|
+
await import(platformEntry);
|
|
26170
26238
|
for (const ctx of ctxPackages) {
|
|
26171
26239
|
const serverDist = join7(ctx.path, "dist", "server", "main", "index.js");
|
|
26172
26240
|
if (!existsSync6(serverDist)) {
|
|
@@ -26179,8 +26247,17 @@ async function loadServerContext(packages) {
|
|
|
26179
26247
|
err(`Failed to load server context from ${ctx.name}: ${e}`);
|
|
26180
26248
|
}
|
|
26181
26249
|
}
|
|
26182
|
-
const
|
|
26183
|
-
|
|
26250
|
+
const nonCtxPackages = packages.filter((p) => !isContextPackage(p.packageJson));
|
|
26251
|
+
for (const pkg of nonCtxPackages) {
|
|
26252
|
+
try {
|
|
26253
|
+
await import(pkg.entrypoint);
|
|
26254
|
+
} catch {}
|
|
26255
|
+
}
|
|
26256
|
+
const { getContext, getAllModuleAccess } = await import(platformEntry);
|
|
26257
|
+
return {
|
|
26258
|
+
context: getContext() ?? null,
|
|
26259
|
+
moduleAccess: getAllModuleAccess()
|
|
26260
|
+
};
|
|
26184
26261
|
}
|
|
26185
26262
|
|
|
26186
26263
|
// src/commands/platform-build.ts
|
|
@@ -26389,8 +26466,33 @@ class ContextHandler {
|
|
|
26389
26466
|
if (this.initialized)
|
|
26390
26467
|
return;
|
|
26391
26468
|
await this.model.init();
|
|
26469
|
+
await this.runSeeds();
|
|
26392
26470
|
this.initialized = true;
|
|
26393
26471
|
}
|
|
26472
|
+
async runSeeds() {
|
|
26473
|
+
for (const element of this.context.elements) {
|
|
26474
|
+
if (!("getSeeds" in element))
|
|
26475
|
+
continue;
|
|
26476
|
+
const seedInfo = element.getSeeds();
|
|
26477
|
+
if (!seedInfo)
|
|
26478
|
+
continue;
|
|
26479
|
+
const { data, idFactory } = seedInfo;
|
|
26480
|
+
const tableName = element.name;
|
|
26481
|
+
const store = this.dataStorage.getStore(tableName);
|
|
26482
|
+
const existing = await store.find({});
|
|
26483
|
+
if (existing.length > 0)
|
|
26484
|
+
continue;
|
|
26485
|
+
const changes = data.map((row) => ({
|
|
26486
|
+
type: "set",
|
|
26487
|
+
data: {
|
|
26488
|
+
...row,
|
|
26489
|
+
_id: row._id ?? idFactory.generate()
|
|
26490
|
+
}
|
|
26491
|
+
}));
|
|
26492
|
+
await store.applyChanges(changes);
|
|
26493
|
+
console.log(`[ARC:Seed] Seeded ${data.length} row(s) into ${tableName}`);
|
|
26494
|
+
}
|
|
26495
|
+
}
|
|
26394
26496
|
async executeCommand(commandName, params, rawToken) {
|
|
26395
26497
|
const scoped = new ScopedModel(this.model, "request");
|
|
26396
26498
|
if (rawToken)
|
|
@@ -27608,6 +27710,9 @@ function generateShellHtml(appName, manifest) {
|
|
|
27608
27710
|
"@arcote.tech/arc": "/shell/arc.js",
|
|
27609
27711
|
"@arcote.tech/arc-ds": "/shell/arc-ds.js",
|
|
27610
27712
|
"@arcote.tech/arc-react": "/shell/arc-react.js",
|
|
27713
|
+
"@arcote.tech/arc-auth": "/shell/arc-auth.js",
|
|
27714
|
+
"@arcote.tech/arc-utils": "/shell/arc-utils.js",
|
|
27715
|
+
"@arcote.tech/arc-workspace": "/shell/arc-workspace.js",
|
|
27611
27716
|
"@arcote.tech/platform": "/shell/platform.js"
|
|
27612
27717
|
}
|
|
27613
27718
|
};
|
|
@@ -27662,16 +27767,69 @@ function serveFile(filePath, headers = {}) {
|
|
|
27662
27767
|
headers: { "Content-Type": getMime(filePath), ...headers }
|
|
27663
27768
|
});
|
|
27664
27769
|
}
|
|
27665
|
-
|
|
27770
|
+
var MODULE_SIG_SECRET = process.env.ARC_MODULE_SECRET ?? crypto.randomUUID();
|
|
27771
|
+
var MODULE_SIG_TTL = 3600;
|
|
27772
|
+
function signModuleUrl(filename) {
|
|
27773
|
+
const exp = Math.floor(Date.now() / 1000) + MODULE_SIG_TTL;
|
|
27774
|
+
const hasher = new Bun.CryptoHasher("sha256");
|
|
27775
|
+
hasher.update(`${filename}:${exp}:${MODULE_SIG_SECRET}`);
|
|
27776
|
+
const sig = hasher.digest("hex").slice(0, 16);
|
|
27777
|
+
return `/modules/${filename}?sig=${sig}&exp=${exp}`;
|
|
27778
|
+
}
|
|
27779
|
+
function verifyModuleSignature(filename, sig, exp) {
|
|
27780
|
+
if (!sig || !exp)
|
|
27781
|
+
return false;
|
|
27782
|
+
if (Number(exp) < Date.now() / 1000)
|
|
27783
|
+
return false;
|
|
27784
|
+
const hasher = new Bun.CryptoHasher("sha256");
|
|
27785
|
+
hasher.update(`${filename}:${exp}:${MODULE_SIG_SECRET}`);
|
|
27786
|
+
return hasher.digest("hex").slice(0, 16) === sig;
|
|
27787
|
+
}
|
|
27788
|
+
async function filterManifestForToken(manifest, moduleAccessMap, tokenPayload) {
|
|
27789
|
+
const filtered = [];
|
|
27790
|
+
for (const mod of manifest.modules) {
|
|
27791
|
+
const access = moduleAccessMap.get(mod.name);
|
|
27792
|
+
if (!access) {
|
|
27793
|
+
filtered.push(mod);
|
|
27794
|
+
continue;
|
|
27795
|
+
}
|
|
27796
|
+
if (!tokenPayload)
|
|
27797
|
+
continue;
|
|
27798
|
+
let granted = false;
|
|
27799
|
+
for (const rule of access.rules) {
|
|
27800
|
+
if (tokenPayload.tokenType === rule.token.name) {
|
|
27801
|
+
granted = rule.check ? await rule.check(tokenPayload) : true;
|
|
27802
|
+
if (granted)
|
|
27803
|
+
break;
|
|
27804
|
+
}
|
|
27805
|
+
}
|
|
27806
|
+
if (granted) {
|
|
27807
|
+
filtered.push({ ...mod, url: signModuleUrl(mod.file) });
|
|
27808
|
+
}
|
|
27809
|
+
}
|
|
27810
|
+
return { modules: filtered, buildTime: manifest.buildTime };
|
|
27811
|
+
}
|
|
27812
|
+
function staticFilesHandler(ws, devMode, moduleAccessMap) {
|
|
27666
27813
|
return (_req, url, ctx) => {
|
|
27667
27814
|
const path4 = url.pathname;
|
|
27668
27815
|
if (path4.startsWith("/shell/"))
|
|
27669
27816
|
return serveFile(join8(ws.shellDir, path4.slice(7)), ctx.corsHeaders);
|
|
27670
|
-
if (path4.startsWith("/modules/"))
|
|
27671
|
-
|
|
27817
|
+
if (path4.startsWith("/modules/")) {
|
|
27818
|
+
const fileWithParams = path4.slice(9);
|
|
27819
|
+
const filename = fileWithParams.split("?")[0];
|
|
27820
|
+
const moduleName = filename.replace(/\.js$/, "");
|
|
27821
|
+
if (moduleAccessMap.has(moduleName)) {
|
|
27822
|
+
const sig = url.searchParams.get("sig");
|
|
27823
|
+
const exp = url.searchParams.get("exp");
|
|
27824
|
+
if (!verifyModuleSignature(filename, sig, exp)) {
|
|
27825
|
+
return new Response("Forbidden", { status: 403, headers: ctx.corsHeaders });
|
|
27826
|
+
}
|
|
27827
|
+
}
|
|
27828
|
+
return serveFile(join8(ws.modulesDir, filename), {
|
|
27672
27829
|
...ctx.corsHeaders,
|
|
27673
27830
|
"Cache-Control": devMode ? "no-cache" : "max-age=31536000,immutable"
|
|
27674
27831
|
});
|
|
27832
|
+
}
|
|
27675
27833
|
if (path4.startsWith("/locales/"))
|
|
27676
27834
|
return serveFile(join8(ws.arcDir, path4.slice(1)), ctx.corsHeaders);
|
|
27677
27835
|
if (path4 === "/styles.css")
|
|
@@ -27689,10 +27847,11 @@ function staticFilesHandler(ws, devMode) {
|
|
|
27689
27847
|
return null;
|
|
27690
27848
|
};
|
|
27691
27849
|
}
|
|
27692
|
-
function apiEndpointsHandler(ws, getManifest, cm) {
|
|
27850
|
+
function apiEndpointsHandler(ws, getManifest, cm, moduleAccessMap) {
|
|
27693
27851
|
return (_req, url, ctx) => {
|
|
27694
|
-
if (url.pathname === "/api/modules")
|
|
27695
|
-
return
|
|
27852
|
+
if (url.pathname === "/api/modules") {
|
|
27853
|
+
return filterManifestForToken(getManifest(), moduleAccessMap, ctx.tokenPayload).then((filtered) => Response.json(filtered, { headers: ctx.corsHeaders }));
|
|
27854
|
+
}
|
|
27696
27855
|
if (url.pathname === "/api/translations") {
|
|
27697
27856
|
const config = readTranslationsConfig(ws.rootDir);
|
|
27698
27857
|
return Response.json(config ?? { locales: [], sourceLocale: "" }, {
|
|
@@ -27743,6 +27902,7 @@ function spaFallbackHandler(shellHtml) {
|
|
|
27743
27902
|
}
|
|
27744
27903
|
async function startPlatformServer(opts) {
|
|
27745
27904
|
const { ws, port, devMode, context } = opts;
|
|
27905
|
+
const moduleAccessMap = opts.moduleAccess ?? new Map;
|
|
27746
27906
|
let manifest = opts.manifest;
|
|
27747
27907
|
const getManifest = () => manifest;
|
|
27748
27908
|
const shellHtml = generateShellHtml(ws.appName, ws.manifest);
|
|
@@ -27765,9 +27925,9 @@ async function startPlatformServer(opts) {
|
|
|
27765
27925
|
corsHeaders: cors
|
|
27766
27926
|
};
|
|
27767
27927
|
const handlers = [
|
|
27768
|
-
apiEndpointsHandler(ws, getManifest, null),
|
|
27928
|
+
apiEndpointsHandler(ws, getManifest, null, moduleAccessMap),
|
|
27769
27929
|
...devMode ? [devReloadHandler(sseClients)] : [],
|
|
27770
|
-
staticFilesHandler(ws, !!devMode),
|
|
27930
|
+
staticFilesHandler(ws, !!devMode, moduleAccessMap),
|
|
27771
27931
|
spaFallbackHandler(shellHtml)
|
|
27772
27932
|
];
|
|
27773
27933
|
for (const handler of handlers) {
|
|
@@ -27812,9 +27972,9 @@ async function startPlatformServer(opts) {
|
|
|
27812
27972
|
dbAdapterFactory: createBunSQLiteAdapterFactory2(dbPath),
|
|
27813
27973
|
port,
|
|
27814
27974
|
httpHandlers: [
|
|
27815
|
-
apiEndpointsHandler(ws, getManifest, null),
|
|
27975
|
+
apiEndpointsHandler(ws, getManifest, null, moduleAccessMap),
|
|
27816
27976
|
...devMode ? [devReloadHandler(sseClients)] : [],
|
|
27817
|
-
staticFilesHandler(ws, !!devMode),
|
|
27977
|
+
staticFilesHandler(ws, !!devMode, moduleAccessMap),
|
|
27818
27978
|
spaFallbackHandler(shellHtml)
|
|
27819
27979
|
],
|
|
27820
27980
|
onWsClose: (clientId) => cleanupClientSubs(clientId)
|
|
@@ -27848,7 +28008,7 @@ async function platformDev() {
|
|
|
27848
28008
|
const port = 5005;
|
|
27849
28009
|
let manifest = await buildAll(ws);
|
|
27850
28010
|
log2("Loading server context...");
|
|
27851
|
-
const context = await loadServerContext(ws.packages);
|
|
28011
|
+
const { context, moduleAccess } = await loadServerContext(ws.packages);
|
|
27852
28012
|
if (context) {
|
|
27853
28013
|
ok("Context loaded");
|
|
27854
28014
|
} else {
|
|
@@ -27859,6 +28019,7 @@ async function platformDev() {
|
|
|
27859
28019
|
port,
|
|
27860
28020
|
manifest,
|
|
27861
28021
|
context,
|
|
28022
|
+
moduleAccess,
|
|
27862
28023
|
dbPath: join9(ws.rootDir, ".arc", "data", "dev.db"),
|
|
27863
28024
|
devMode: true
|
|
27864
28025
|
});
|
|
@@ -27938,7 +28099,7 @@ async function platformStart() {
|
|
|
27938
28099
|
}
|
|
27939
28100
|
const manifest = JSON.parse(readFileSync7(manifestPath, "utf-8"));
|
|
27940
28101
|
log2("Loading server context...");
|
|
27941
|
-
const context = await loadServerContext(ws.packages);
|
|
28102
|
+
const { context, moduleAccess } = await loadServerContext(ws.packages);
|
|
27942
28103
|
if (context) {
|
|
27943
28104
|
ok("Context loaded");
|
|
27944
28105
|
} else {
|
|
@@ -27949,6 +28110,7 @@ async function platformStart() {
|
|
|
27949
28110
|
port,
|
|
27950
28111
|
manifest,
|
|
27951
28112
|
context,
|
|
28113
|
+
moduleAccess,
|
|
27952
28114
|
dbPath: join10(ws.rootDir, ".arc", "data", "prod.db"),
|
|
27953
28115
|
devMode: false
|
|
27954
28116
|
});
|
package/package.json
CHANGED
|
@@ -84,6 +84,9 @@ export const SHELL_EXTERNALS = [
|
|
|
84
84
|
"@arcote.tech/arc",
|
|
85
85
|
"@arcote.tech/arc-ds",
|
|
86
86
|
"@arcote.tech/arc-react",
|
|
87
|
+
"@arcote.tech/arc-auth",
|
|
88
|
+
"@arcote.tech/arc-utils",
|
|
89
|
+
"@arcote.tech/arc-workspace",
|
|
87
90
|
"@arcote.tech/platform",
|
|
88
91
|
];
|
|
89
92
|
|
|
@@ -94,8 +97,13 @@ export interface WorkspacePackage {
|
|
|
94
97
|
packageJson: Record<string, any>;
|
|
95
98
|
}
|
|
96
99
|
|
|
100
|
+
export interface ModuleEntry {
|
|
101
|
+
file: string;
|
|
102
|
+
name: string;
|
|
103
|
+
}
|
|
104
|
+
|
|
97
105
|
export interface BuildManifest {
|
|
98
|
-
modules:
|
|
106
|
+
modules: ModuleEntry[];
|
|
99
107
|
buildTime: string;
|
|
100
108
|
}
|
|
101
109
|
|
|
@@ -200,9 +208,13 @@ export async function buildPackages(
|
|
|
200
208
|
mkdirSync(tmpDir, { recursive: true });
|
|
201
209
|
|
|
202
210
|
const entrypoints: string[] = [];
|
|
211
|
+
const fileToName = new Map<string, string>(); // safeName → module name
|
|
203
212
|
|
|
204
213
|
for (const pkg of packages) {
|
|
205
214
|
const safeName = pkg.path.split("/").pop()!;
|
|
215
|
+
// Module name: strip scope (e.g. @ndt/admin → admin)
|
|
216
|
+
const moduleName = pkg.name.includes("/") ? pkg.name.split("/").pop()! : pkg.name;
|
|
217
|
+
fileToName.set(safeName, moduleName);
|
|
206
218
|
|
|
207
219
|
// All packages get a simple re-export wrapper.
|
|
208
220
|
// Context packages that use module().build() self-register via side effects.
|
|
@@ -217,6 +229,17 @@ export async function buildPackages(
|
|
|
217
229
|
// i18n extraction — collect translatable strings during bundling
|
|
218
230
|
const i18nCollector = new Map<string, Set<string>>();
|
|
219
231
|
|
|
232
|
+
// Plugin to force all @arcote.tech/* imports as external,
|
|
233
|
+
// even when resolved through symlinks (bun link)
|
|
234
|
+
const arcExternalPlugin: import("bun").BunPlugin = {
|
|
235
|
+
name: "arc-external",
|
|
236
|
+
setup(build) {
|
|
237
|
+
build.onResolve({ filter: /^@arcote\.tech\// }, (args) => {
|
|
238
|
+
return { path: args.path, external: true };
|
|
239
|
+
});
|
|
240
|
+
},
|
|
241
|
+
};
|
|
242
|
+
|
|
220
243
|
const result = await Bun.build({
|
|
221
244
|
entrypoints,
|
|
222
245
|
outdir: outDir,
|
|
@@ -224,7 +247,7 @@ export async function buildPackages(
|
|
|
224
247
|
format: "esm",
|
|
225
248
|
target: "browser",
|
|
226
249
|
external: SHELL_EXTERNALS,
|
|
227
|
-
plugins: [i18nExtractPlugin(i18nCollector, rootDir)],
|
|
250
|
+
plugins: [arcExternalPlugin, i18nExtractPlugin(i18nCollector, rootDir)],
|
|
228
251
|
naming: "[name].[ext]",
|
|
229
252
|
define: {
|
|
230
253
|
ONLY_SERVER: "false",
|
|
@@ -248,12 +271,16 @@ export async function buildPackages(
|
|
|
248
271
|
rmSync(tmpDir, { recursive: true, force: true });
|
|
249
272
|
|
|
250
273
|
// Build manifest
|
|
251
|
-
const
|
|
274
|
+
const moduleEntries: ModuleEntry[] = result.outputs
|
|
252
275
|
.filter((o) => o.kind === "entry-point")
|
|
253
|
-
.map((o) =>
|
|
276
|
+
.map((o) => {
|
|
277
|
+
const file = o.path.split("/").pop()!;
|
|
278
|
+
const safeName = file.replace(/\.js$/, "");
|
|
279
|
+
return { file, name: fileToName.get(safeName) ?? safeName };
|
|
280
|
+
});
|
|
254
281
|
|
|
255
282
|
const manifest: BuildManifest = {
|
|
256
|
-
modules:
|
|
283
|
+
modules: moduleEntries,
|
|
257
284
|
buildTime: new Date().toISOString(),
|
|
258
285
|
};
|
|
259
286
|
|
|
@@ -21,7 +21,7 @@ export async function platformDev(): Promise<void> {
|
|
|
21
21
|
|
|
22
22
|
// Load server context
|
|
23
23
|
log("Loading server context...");
|
|
24
|
-
const context = await loadServerContext(ws.packages);
|
|
24
|
+
const { context, moduleAccess } = await loadServerContext(ws.packages);
|
|
25
25
|
if (context) {
|
|
26
26
|
ok("Context loaded");
|
|
27
27
|
} else {
|
|
@@ -34,6 +34,7 @@ export async function platformDev(): Promise<void> {
|
|
|
34
34
|
port,
|
|
35
35
|
manifest,
|
|
36
36
|
context,
|
|
37
|
+
moduleAccess,
|
|
37
38
|
dbPath: join(ws.rootDir, ".arc", "data", "dev.db"),
|
|
38
39
|
devMode: true,
|
|
39
40
|
});
|
|
@@ -26,7 +26,7 @@ export async function platformStart(): Promise<void> {
|
|
|
26
26
|
|
|
27
27
|
// Load server context
|
|
28
28
|
log("Loading server context...");
|
|
29
|
-
const context = await loadServerContext(ws.packages);
|
|
29
|
+
const { context, moduleAccess } = await loadServerContext(ws.packages);
|
|
30
30
|
if (context) {
|
|
31
31
|
ok("Context loaded");
|
|
32
32
|
} else {
|
|
@@ -39,6 +39,7 @@ export async function platformStart(): Promise<void> {
|
|
|
39
39
|
port,
|
|
40
40
|
manifest,
|
|
41
41
|
context,
|
|
42
|
+
moduleAccess,
|
|
42
43
|
dbPath: join(ws.rootDir, ".arc", "data", "prod.db"),
|
|
43
44
|
devMode: false,
|
|
44
45
|
});
|
package/src/i18n/catalog.ts
CHANGED
|
@@ -18,6 +18,7 @@ export interface CatalogEntry {
|
|
|
18
18
|
locations: string[];
|
|
19
19
|
hash: string;
|
|
20
20
|
obsolete: boolean;
|
|
21
|
+
manual?: boolean;
|
|
21
22
|
}
|
|
22
23
|
|
|
23
24
|
/** Short hash from msgid for change tracking */
|
|
@@ -37,6 +38,7 @@ export function parsePo(content: string): CatalogEntry[] {
|
|
|
37
38
|
let msgid = "";
|
|
38
39
|
let msgstr = "";
|
|
39
40
|
let obsolete = false;
|
|
41
|
+
let inManual = false;
|
|
40
42
|
|
|
41
43
|
const flush = () => {
|
|
42
44
|
if (msgid) {
|
|
@@ -46,6 +48,7 @@ export function parsePo(content: string): CatalogEntry[] {
|
|
|
46
48
|
locations,
|
|
47
49
|
hash: hash || hashMsgid(msgid),
|
|
48
50
|
obsolete,
|
|
51
|
+
...(inManual ? { manual: true } : {}),
|
|
49
52
|
});
|
|
50
53
|
}
|
|
51
54
|
locations = [];
|
|
@@ -58,6 +61,17 @@ export function parsePo(content: string): CatalogEntry[] {
|
|
|
58
61
|
for (const line of lines) {
|
|
59
62
|
const trimmed = line.trim();
|
|
60
63
|
|
|
64
|
+
// Manual section markers
|
|
65
|
+
if (trimmed === "# manual") {
|
|
66
|
+
inManual = true;
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
if (trimmed === "# end manual") {
|
|
70
|
+
if (msgid) flush();
|
|
71
|
+
inManual = false;
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
|
|
61
75
|
if (trimmed === "" || trimmed.startsWith("#,")) {
|
|
62
76
|
// Empty line or flags — boundary between entries
|
|
63
77
|
if (msgid) flush();
|
|
@@ -110,10 +124,23 @@ export function parsePo(content: string): CatalogEntry[] {
|
|
|
110
124
|
export function writePo(entries: CatalogEntry[]): string {
|
|
111
125
|
const lines: string[] = [];
|
|
112
126
|
|
|
113
|
-
|
|
114
|
-
const active = entries.filter((e) => !e.obsolete);
|
|
127
|
+
const manual = entries.filter((e) => e.manual);
|
|
128
|
+
const active = entries.filter((e) => !e.obsolete && !e.manual);
|
|
115
129
|
const obsolete = entries.filter((e) => e.obsolete);
|
|
116
130
|
|
|
131
|
+
// Manual section
|
|
132
|
+
if (manual.length > 0) {
|
|
133
|
+
lines.push("# manual");
|
|
134
|
+
for (const entry of manual) {
|
|
135
|
+
lines.push(`msgid ${quoteString(entry.msgid)}`);
|
|
136
|
+
lines.push(`msgstr ${quoteString(entry.msgstr)}`);
|
|
137
|
+
lines.push("");
|
|
138
|
+
}
|
|
139
|
+
lines.push("# end manual");
|
|
140
|
+
lines.push("");
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Extracted entries
|
|
117
144
|
for (const entry of active) {
|
|
118
145
|
for (const loc of entry.locations) {
|
|
119
146
|
lines.push(`#: ${loc}`);
|
|
@@ -124,6 +151,7 @@ export function writePo(entries: CatalogEntry[]): string {
|
|
|
124
151
|
lines.push("");
|
|
125
152
|
}
|
|
126
153
|
|
|
154
|
+
// Obsolete entries
|
|
127
155
|
if (obsolete.length > 0) {
|
|
128
156
|
lines.push("# Obsolete entries");
|
|
129
157
|
lines.push("");
|
|
@@ -152,12 +180,18 @@ export function mergeCatalog(
|
|
|
152
180
|
existingMap.set(entry.msgid, entry);
|
|
153
181
|
}
|
|
154
182
|
|
|
183
|
+
// Preserve manual entries as-is
|
|
184
|
+
const manualEntries = existing.filter((e) => e.manual);
|
|
185
|
+
const manualIds = new Set(manualEntries.map((e) => e.msgid));
|
|
186
|
+
|
|
155
187
|
const result: CatalogEntry[] = [];
|
|
156
188
|
const seen = new Set<string>();
|
|
157
189
|
|
|
158
|
-
// Process all extracted messages
|
|
190
|
+
// Process all extracted messages (skip those in manual section)
|
|
159
191
|
for (const [msgid, locations] of extracted) {
|
|
160
192
|
seen.add(msgid);
|
|
193
|
+
if (manualIds.has(msgid)) continue;
|
|
194
|
+
|
|
161
195
|
const prev = existingMap.get(msgid);
|
|
162
196
|
|
|
163
197
|
result.push({
|
|
@@ -171,7 +205,7 @@ export function mergeCatalog(
|
|
|
171
205
|
|
|
172
206
|
// Mark removed entries as obsolete (keep their translations)
|
|
173
207
|
for (const entry of existing) {
|
|
174
|
-
if (!seen.has(entry.msgid) && !entry.obsolete && entry.msgstr) {
|
|
208
|
+
if (!seen.has(entry.msgid) && !entry.obsolete && !entry.manual && entry.msgstr) {
|
|
175
209
|
result.push({
|
|
176
210
|
...entry,
|
|
177
211
|
obsolete: true,
|
|
@@ -180,7 +214,11 @@ export function mergeCatalog(
|
|
|
180
214
|
}
|
|
181
215
|
}
|
|
182
216
|
|
|
183
|
-
|
|
217
|
+
// Sort extracted entries alphabetically for deterministic output
|
|
218
|
+
result.sort((a, b) => a.msgid.localeCompare(b.msgid));
|
|
219
|
+
|
|
220
|
+
// Manual entries first, then sorted extracted, then obsolete
|
|
221
|
+
return [...manualEntries, ...result];
|
|
184
222
|
}
|
|
185
223
|
|
|
186
224
|
function extractQuoted(s: string): string {
|
package/src/i18n/compile.ts
CHANGED
|
@@ -18,7 +18,12 @@ export function compileCatalog(poPath: string): Record<string, string> {
|
|
|
18
18
|
}
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
-
|
|
21
|
+
// Sort keys for deterministic JSON output
|
|
22
|
+
const sorted: Record<string, string> = {};
|
|
23
|
+
for (const key of Object.keys(result).sort()) {
|
|
24
|
+
sorted[key] = result[key];
|
|
25
|
+
}
|
|
26
|
+
return sorted;
|
|
22
27
|
}
|
|
23
28
|
|
|
24
29
|
/**
|
package/src/platform/server.ts
CHANGED
|
@@ -11,7 +11,8 @@ import {
|
|
|
11
11
|
import { existsSync, mkdirSync } from "fs";
|
|
12
12
|
import { join } from "path";
|
|
13
13
|
import { readTranslationsConfig } from "../i18n";
|
|
14
|
-
import type { BuildManifest, WorkspaceInfo } from "./shared";
|
|
14
|
+
import type { BuildManifest, ModuleEntry, WorkspaceInfo } from "./shared";
|
|
15
|
+
import type { ModuleAccess } from "@arcote.tech/platform";
|
|
15
16
|
|
|
16
17
|
// ---------------------------------------------------------------------------
|
|
17
18
|
// Types
|
|
@@ -23,6 +24,8 @@ export interface PlatformServerOptions {
|
|
|
23
24
|
manifest: BuildManifest;
|
|
24
25
|
/** Server context (from loadServerContext). If null, static-only mode. */
|
|
25
26
|
context?: any;
|
|
27
|
+
/** Module access rules (from registry after loadServerContext). */
|
|
28
|
+
moduleAccess?: Map<string, ModuleAccess>;
|
|
26
29
|
/** Path to SQLite database file */
|
|
27
30
|
dbPath?: string;
|
|
28
31
|
/** If true, enables SSE reload stream + mutable manifest (dev mode) */
|
|
@@ -77,6 +80,9 @@ export function generateShellHtml(appName: string, manifest?: { title: string; f
|
|
|
77
80
|
"@arcote.tech/arc": "/shell/arc.js",
|
|
78
81
|
"@arcote.tech/arc-ds": "/shell/arc-ds.js",
|
|
79
82
|
"@arcote.tech/arc-react": "/shell/arc-react.js",
|
|
83
|
+
"@arcote.tech/arc-auth": "/shell/arc-auth.js",
|
|
84
|
+
"@arcote.tech/arc-utils": "/shell/arc-utils.js",
|
|
85
|
+
"@arcote.tech/arc-workspace": "/shell/arc-workspace.js",
|
|
80
86
|
"@arcote.tech/platform": "/shell/platform.js",
|
|
81
87
|
},
|
|
82
88
|
};
|
|
@@ -141,20 +147,96 @@ function serveFile(
|
|
|
141
147
|
});
|
|
142
148
|
}
|
|
143
149
|
|
|
150
|
+
// ---------------------------------------------------------------------------
|
|
151
|
+
// Module access — signed URLs
|
|
152
|
+
// ---------------------------------------------------------------------------
|
|
153
|
+
|
|
154
|
+
const MODULE_SIG_SECRET = process.env.ARC_MODULE_SECRET ?? crypto.randomUUID();
|
|
155
|
+
const MODULE_SIG_TTL = 3600; // 1 hour
|
|
156
|
+
|
|
157
|
+
function signModuleUrl(filename: string): string {
|
|
158
|
+
const exp = Math.floor(Date.now() / 1000) + MODULE_SIG_TTL;
|
|
159
|
+
const hasher = new Bun.CryptoHasher("sha256");
|
|
160
|
+
hasher.update(`${filename}:${exp}:${MODULE_SIG_SECRET}`);
|
|
161
|
+
const sig = hasher.digest("hex").slice(0, 16);
|
|
162
|
+
return `/modules/${filename}?sig=${sig}&exp=${exp}`;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function verifyModuleSignature(filename: string, sig: string | null, exp: string | null): boolean {
|
|
166
|
+
if (!sig || !exp) return false;
|
|
167
|
+
if (Number(exp) < Date.now() / 1000) return false;
|
|
168
|
+
const hasher = new Bun.CryptoHasher("sha256");
|
|
169
|
+
hasher.update(`${filename}:${exp}:${MODULE_SIG_SECRET}`);
|
|
170
|
+
return hasher.digest("hex").slice(0, 16) === sig;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
async function filterManifestForToken(
|
|
174
|
+
manifest: BuildManifest,
|
|
175
|
+
moduleAccessMap: Map<string, ModuleAccess>,
|
|
176
|
+
tokenPayload: any,
|
|
177
|
+
): Promise<BuildManifest> {
|
|
178
|
+
const filtered: ModuleEntry[] = [];
|
|
179
|
+
|
|
180
|
+
for (const mod of manifest.modules) {
|
|
181
|
+
const access = moduleAccessMap.get(mod.name);
|
|
182
|
+
|
|
183
|
+
if (!access) {
|
|
184
|
+
// Public module — always include
|
|
185
|
+
filtered.push(mod);
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Protected module — check if token grants access
|
|
190
|
+
if (!tokenPayload) continue;
|
|
191
|
+
|
|
192
|
+
let granted = false;
|
|
193
|
+
for (const rule of access.rules) {
|
|
194
|
+
if (tokenPayload.tokenType === rule.token.name) {
|
|
195
|
+
granted = rule.check ? await rule.check(tokenPayload) : true;
|
|
196
|
+
if (granted) break;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
if (granted) {
|
|
201
|
+
filtered.push({ ...mod, url: signModuleUrl(mod.file) } as any);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
return { modules: filtered, buildTime: manifest.buildTime };
|
|
206
|
+
}
|
|
207
|
+
|
|
144
208
|
// ---------------------------------------------------------------------------
|
|
145
209
|
// Platform-specific HTTP handlers
|
|
146
210
|
// ---------------------------------------------------------------------------
|
|
147
211
|
|
|
148
|
-
function staticFilesHandler(
|
|
212
|
+
function staticFilesHandler(
|
|
213
|
+
ws: WorkspaceInfo,
|
|
214
|
+
devMode: boolean,
|
|
215
|
+
moduleAccessMap: Map<string, ModuleAccess>,
|
|
216
|
+
): ArcHttpHandler {
|
|
149
217
|
return (_req, url, ctx) => {
|
|
150
218
|
const path = url.pathname;
|
|
151
219
|
if (path.startsWith("/shell/"))
|
|
152
220
|
return serveFile(join(ws.shellDir, path.slice(7)), ctx.corsHeaders);
|
|
153
|
-
if (path.startsWith("/modules/"))
|
|
154
|
-
|
|
221
|
+
if (path.startsWith("/modules/")) {
|
|
222
|
+
const fileWithParams = path.slice(9);
|
|
223
|
+
const filename = fileWithParams.split("?")[0];
|
|
224
|
+
const moduleName = filename.replace(/\.js$/, "");
|
|
225
|
+
|
|
226
|
+
// Check access for protected modules
|
|
227
|
+
if (moduleAccessMap.has(moduleName)) {
|
|
228
|
+
const sig = url.searchParams.get("sig");
|
|
229
|
+
const exp = url.searchParams.get("exp");
|
|
230
|
+
if (!verifyModuleSignature(filename, sig, exp)) {
|
|
231
|
+
return new Response("Forbidden", { status: 403, headers: ctx.corsHeaders });
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
return serveFile(join(ws.modulesDir, filename), {
|
|
155
236
|
...ctx.corsHeaders,
|
|
156
237
|
"Cache-Control": devMode ? "no-cache" : "max-age=31536000,immutable",
|
|
157
238
|
});
|
|
239
|
+
}
|
|
158
240
|
if (path.startsWith("/locales/"))
|
|
159
241
|
return serveFile(join(ws.arcDir, path.slice(1)), ctx.corsHeaders);
|
|
160
242
|
if (path === "/styles.css")
|
|
@@ -182,10 +264,14 @@ function apiEndpointsHandler(
|
|
|
182
264
|
ws: WorkspaceInfo,
|
|
183
265
|
getManifest: () => BuildManifest,
|
|
184
266
|
cm: ConnectionManager | null,
|
|
267
|
+
moduleAccessMap: Map<string, ModuleAccess>,
|
|
185
268
|
): ArcHttpHandler {
|
|
186
269
|
return (_req, url, ctx) => {
|
|
187
|
-
if (url.pathname === "/api/modules")
|
|
188
|
-
|
|
270
|
+
if (url.pathname === "/api/modules") {
|
|
271
|
+
// Filter manifest based on token — protected modules only for authorized users
|
|
272
|
+
return filterManifestForToken(getManifest(), moduleAccessMap, ctx.tokenPayload)
|
|
273
|
+
.then((filtered) => Response.json(filtered, { headers: ctx.corsHeaders }));
|
|
274
|
+
}
|
|
189
275
|
|
|
190
276
|
if (url.pathname === "/api/translations") {
|
|
191
277
|
const config = readTranslationsConfig(ws.rootDir);
|
|
@@ -252,6 +338,7 @@ export async function startPlatformServer(
|
|
|
252
338
|
opts: PlatformServerOptions,
|
|
253
339
|
): Promise<PlatformServer> {
|
|
254
340
|
const { ws, port, devMode, context } = opts;
|
|
341
|
+
const moduleAccessMap = opts.moduleAccess ?? new Map();
|
|
255
342
|
let manifest = opts.manifest;
|
|
256
343
|
const getManifest = () => manifest;
|
|
257
344
|
|
|
@@ -282,9 +369,9 @@ export async function startPlatformServer(
|
|
|
282
369
|
|
|
283
370
|
// Platform handlers only
|
|
284
371
|
const handlers: ArcHttpHandler[] = [
|
|
285
|
-
apiEndpointsHandler(ws, getManifest, null),
|
|
372
|
+
apiEndpointsHandler(ws, getManifest, null, moduleAccessMap),
|
|
286
373
|
...(devMode ? [devReloadHandler(sseClients)] : []),
|
|
287
|
-
staticFilesHandler(ws, !!devMode),
|
|
374
|
+
staticFilesHandler(ws, !!devMode, moduleAccessMap),
|
|
288
375
|
spaFallbackHandler(shellHtml),
|
|
289
376
|
];
|
|
290
377
|
|
|
@@ -328,9 +415,9 @@ export async function startPlatformServer(
|
|
|
328
415
|
port,
|
|
329
416
|
httpHandlers: [
|
|
330
417
|
// Platform-specific handlers (checked AFTER arc handlers)
|
|
331
|
-
apiEndpointsHandler(ws, getManifest, null),
|
|
418
|
+
apiEndpointsHandler(ws, getManifest, null, moduleAccessMap),
|
|
332
419
|
...(devMode ? [devReloadHandler(sseClients)] : []),
|
|
333
|
-
staticFilesHandler(ws, !!devMode),
|
|
420
|
+
staticFilesHandler(ws, !!devMode, moduleAccessMap),
|
|
334
421
|
spaFallbackHandler(shellHtml),
|
|
335
422
|
],
|
|
336
423
|
onWsClose: (clientId) => cleanupClientSubs(clientId),
|
package/src/platform/shared.ts
CHANGED
|
@@ -7,12 +7,13 @@ import {
|
|
|
7
7
|
discoverPackages,
|
|
8
8
|
isContextPackage,
|
|
9
9
|
type BuildManifest,
|
|
10
|
+
type ModuleEntry,
|
|
10
11
|
type WorkspacePackage,
|
|
11
12
|
} from "../builder/module-builder";
|
|
12
13
|
|
|
13
14
|
// Re-export for convenience
|
|
14
15
|
export { buildPackages, buildStyles, isContextPackage };
|
|
15
|
-
export type { BuildManifest, WorkspacePackage };
|
|
16
|
+
export type { BuildManifest, ModuleEntry, WorkspacePackage };
|
|
16
17
|
|
|
17
18
|
// ---------------------------------------------------------------------------
|
|
18
19
|
// Logging
|
|
@@ -206,34 +207,48 @@ export const { createPortal, flushSync } = ReactDOM;`,
|
|
|
206
207
|
["arc", "@arcote.tech/arc"],
|
|
207
208
|
["arc-ds", "@arcote.tech/arc-ds"],
|
|
208
209
|
["arc-react", "@arcote.tech/arc-react"],
|
|
210
|
+
["arc-auth", "@arcote.tech/arc-auth"],
|
|
211
|
+
["arc-utils", "@arcote.tech/arc-utils"],
|
|
212
|
+
["arc-workspace", "@arcote.tech/arc-workspace"],
|
|
209
213
|
["platform", "@arcote.tech/platform"],
|
|
210
214
|
];
|
|
211
215
|
|
|
212
|
-
const
|
|
216
|
+
const baseExternal = [
|
|
217
|
+
"react",
|
|
218
|
+
"react-dom",
|
|
219
|
+
"react/jsx-runtime",
|
|
220
|
+
"react/jsx-dev-runtime",
|
|
221
|
+
"react-dom/client",
|
|
222
|
+
];
|
|
223
|
+
const allArcPkgs = arcEntries.map(([, pkg]) => pkg);
|
|
224
|
+
|
|
225
|
+
// Build each arc entry separately so it can import sibling arc packages
|
|
226
|
+
// as externals (resolved via import map) without circular self-reference.
|
|
213
227
|
for (const [name, pkg] of arcEntries) {
|
|
214
228
|
const f = join(tmpDir, `${name}.ts`);
|
|
215
229
|
Bun.write(f, `export * from "${pkg}";\n`);
|
|
216
|
-
arcEps.push(f);
|
|
217
|
-
}
|
|
218
230
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
231
|
+
const r2 = await Bun.build({
|
|
232
|
+
entrypoints: [f],
|
|
233
|
+
outdir: outDir,
|
|
234
|
+
format: "esm",
|
|
235
|
+
target: "browser",
|
|
236
|
+
naming: "[name].[ext]",
|
|
237
|
+
external: [
|
|
238
|
+
...baseExternal,
|
|
239
|
+
// Other arc packages are external (not self)
|
|
240
|
+
...allArcPkgs.filter((p) => p !== pkg),
|
|
241
|
+
],
|
|
242
|
+
define: {
|
|
243
|
+
ONLY_SERVER: "false",
|
|
244
|
+
ONLY_BROWSER: "true",
|
|
245
|
+
ONLY_CLIENT: "true",
|
|
246
|
+
},
|
|
247
|
+
});
|
|
248
|
+
if (!r2.success) {
|
|
249
|
+
for (const l of r2.logs) console.error(l);
|
|
250
|
+
throw new Error(`Shell build failed for ${pkg}`);
|
|
251
|
+
}
|
|
237
252
|
}
|
|
238
253
|
|
|
239
254
|
// Clean tmp
|
|
@@ -247,9 +262,9 @@ export const { createPortal, flushSync } = ReactDOM;`,
|
|
|
247
262
|
|
|
248
263
|
export async function loadServerContext(
|
|
249
264
|
packages: WorkspacePackage[],
|
|
250
|
-
): Promise<any | null> {
|
|
265
|
+
): Promise<{ context: any | null; moduleAccess: Map<string, any> }> {
|
|
251
266
|
const ctxPackages = packages.filter((p) => isContextPackage(p.packageJson));
|
|
252
|
-
if (ctxPackages.length === 0) return null;
|
|
267
|
+
if (ctxPackages.length === 0) return { context: null, moduleAccess: new Map() };
|
|
253
268
|
|
|
254
269
|
// Set globals for server context — framework packages (arc-auth etc.)
|
|
255
270
|
// use these at runtime to tree-shake browser/server code paths.
|
|
@@ -259,6 +274,18 @@ export async function loadServerContext(
|
|
|
259
274
|
|
|
260
275
|
// Import all context packages — side effects from module().build()
|
|
261
276
|
// register context elements into the global registry via setContext().
|
|
277
|
+
// Resolve platform from the project's node_modules using an absolute path.
|
|
278
|
+
// When CLI is bun-linked, `import("@arcote.tech/platform")` would resolve to
|
|
279
|
+
// the CLI's own copy, creating a separate module instance from what context
|
|
280
|
+
// packages use. Using an absolute path ensures a single shared instance.
|
|
281
|
+
const platformDir = join(process.cwd(), "node_modules", "@arcote.tech", "platform");
|
|
282
|
+
const platformPkg = JSON.parse(readFileSync(join(platformDir, "package.json"), "utf-8"));
|
|
283
|
+
const platformEntry = join(platformDir, platformPkg.main ?? "src/index.ts");
|
|
284
|
+
|
|
285
|
+
// Pre-import platform so it's cached with this absolute path
|
|
286
|
+
await import(platformEntry);
|
|
287
|
+
|
|
288
|
+
// Import context packages from server dist (has server-only code paths)
|
|
262
289
|
for (const ctx of ctxPackages) {
|
|
263
290
|
const serverDist = join(ctx.path, "dist", "server", "main", "index.js");
|
|
264
291
|
if (!existsSync(serverDist)) {
|
|
@@ -273,8 +300,20 @@ export async function loadServerContext(
|
|
|
273
300
|
}
|
|
274
301
|
}
|
|
275
302
|
|
|
276
|
-
//
|
|
277
|
-
|
|
278
|
-
const
|
|
279
|
-
|
|
303
|
+
// Import non-context packages from source to capture module().protectedBy() metadata
|
|
304
|
+
const nonCtxPackages = packages.filter((p) => !isContextPackage(p.packageJson));
|
|
305
|
+
for (const pkg of nonCtxPackages) {
|
|
306
|
+
try {
|
|
307
|
+
await import(pkg.entrypoint);
|
|
308
|
+
} catch {
|
|
309
|
+
// Non-context packages may fail on server (React components etc.) — that's OK,
|
|
310
|
+
// module().protectedBy().build() runs synchronously before any rendering
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
const { getContext, getAllModuleAccess } = await import(platformEntry);
|
|
315
|
+
return {
|
|
316
|
+
context: getContext() ?? null,
|
|
317
|
+
moduleAccess: getAllModuleAccess(),
|
|
318
|
+
};
|
|
280
319
|
}
|