@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 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 crypto = __require("node:crypto");
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 crypto.createPublicKey === "function";
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 = crypto.createHmac("sha" + bits, secret);
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 crypto ? function timingSafeEqual(a, b) {
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 crypto.timingSafeEqual(a, b);
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 = crypto.createSign("RSA-SHA" + bits);
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 = crypto.createVerify("RSA-SHA" + bits);
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 = crypto.createSign("RSA-SHA" + bits);
9501
+ var signer = crypto2.createSign("RSA-SHA" + bits);
9502
9502
  var sig = (signer.update(thing), signer.sign({
9503
9503
  key: privateKey,
9504
- padding: crypto.constants.RSA_PKCS1_PSS_PADDING,
9505
- saltLength: crypto.constants.RSA_PSS_SALTLEN_DIGEST
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 = crypto.createVerify("RSA-SHA" + bits);
9515
+ var verifier = crypto2.createVerify("RSA-SHA" + bits);
9516
9516
  verifier.update(thing);
9517
9517
  return verifier.verify({
9518
9518
  key: publicKey,
9519
- padding: crypto.constants.RSA_PKCS1_PSS_PADDING,
9520
- saltLength: crypto.constants.RSA_PSS_SALTLEN_DIGEST
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, ArcBoolean, stringValidator, ArcString, ArcId, numberValidator, ArcNumber, ArcContextElement, ArcEvent, ForkedStoreState, ForkedDataStorage, MasterStoreState, MasterDataStorage2, dateValidator, SQLiteReadWriteTransaction, createSQLiteAdapterFactory = (db) => {
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 active = entries.filter((e) => !e.obsolete);
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
- return result;
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
- return result;
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 moduleFiles = result.outputs.filter((o) => o.kind === "entry-point").map((o) => o.path.split("/").pop());
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: moduleFiles,
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 arcEps = [];
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
- arcEps.push(f);
26139
- }
26140
- const r2 = await Bun.build({
26141
- entrypoints: arcEps,
26142
- outdir: outDir,
26143
- splitting: true,
26144
- format: "esm",
26145
- target: "browser",
26146
- naming: "[name].[ext]",
26147
- external: [
26148
- "react",
26149
- "react-dom",
26150
- "react/jsx-runtime",
26151
- "react/jsx-dev-runtime",
26152
- "react-dom/client"
26153
- ]
26154
- });
26155
- if (!r2.success) {
26156
- for (const l of r2.logs)
26157
- console.error(l);
26158
- throw new Error("Shell Arc build failed");
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 { getContext } = await import("@arcote.tech/platform");
26183
- return getContext() ?? null;
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
- function staticFilesHandler(ws, devMode) {
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
- return serveFile(join8(ws.modulesDir, path4.slice(9)), {
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 Response.json(getManifest(), { headers: ctx.corsHeaders });
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@arcote.tech/arc-cli",
3
- "version": "0.4.5",
3
+ "version": "0.4.7",
4
4
  "description": "CLI tool for Arc framework",
5
5
  "module": "index.ts",
6
6
  "main": "dist/index.js",
@@ -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: string[];
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 moduleFiles = result.outputs
274
+ const moduleEntries: ModuleEntry[] = result.outputs
252
275
  .filter((o) => o.kind === "entry-point")
253
- .map((o) => o.path.split("/").pop()!);
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: moduleFiles,
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
  });
@@ -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
- // Active entries first, then obsolete
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
- return result;
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 {
@@ -18,7 +18,12 @@ export function compileCatalog(poPath: string): Record<string, string> {
18
18
  }
19
19
  }
20
20
 
21
- return result;
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
  /**
@@ -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(ws: WorkspaceInfo, devMode: boolean): ArcHttpHandler {
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
- return serveFile(join(ws.modulesDir, path.slice(9)), {
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
- return Response.json(getManifest(), { headers: ctx.corsHeaders });
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),
@@ -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 arcEps: string[] = [];
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
- const r2 = await Bun.build({
220
- entrypoints: arcEps,
221
- outdir: outDir,
222
- splitting: true,
223
- format: "esm",
224
- target: "browser",
225
- naming: "[name].[ext]",
226
- external: [
227
- "react",
228
- "react-dom",
229
- "react/jsx-runtime",
230
- "react/jsx-dev-runtime",
231
- "react-dom/client",
232
- ],
233
- });
234
- if (!r2.success) {
235
- for (const l of r2.logs) console.error(l);
236
- throw new Error("Shell Arc build failed");
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
- // After all imports, module().build() side effects have called setContext()
277
- // in the arc-ui registry. Retrieve the merged context from there.
278
- const { getContext } = await import("@arcote.tech/platform");
279
- return getContext() ?? null;
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
  }