@camstack/system 1.1.43 → 1.1.45

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.
@@ -43,8 +43,14 @@ export declare class AddonWidgetsAggregatorAddon extends BaseAddon {
43
43
  * just gets a fresh URL on each call instead of cache-friendly mtime.
44
44
  */
45
45
  private makeBundleUrl;
46
- /** sha1(content) 12-hex of a bundle entry file, memoised; `Date.now()` when absent. */
47
- private bundleVersionTag;
46
+ /**
47
+ * sha1 (12-hex) over EVERY file in the bundle's dist dir — content, not
48
+ * mtime (reproducible builds fix mtime, so mtime never busts). Memoised by
49
+ * the dir's (name:size:mtime) fingerprint; a redeploy that changes any
50
+ * file's content changes its size and re-hashes. `Date.now()` when the
51
+ * bundle has no local file (remote addon).
52
+ */
53
+ private bundleDirVersionTag;
48
54
  private resolveBundlePath;
49
55
  private resolvePaths;
50
56
  }
@@ -198,19 +198,34 @@ var AddonWidgetsAggregatorAddon = class extends require_dist.BaseAddon {
198
198
  */
199
199
  makeBundleUrl(addonId, bundle) {
200
200
  const bundlePath = this.resolveBundlePath(addonId, bundle);
201
- return `/api/addon-widgets/${addonId}/${bundle}?v=${this.bundleVersionTag(bundlePath)}`;
201
+ return `/api/addon-widgets/${addonId}/v${this.bundleDirVersionTag(bundlePath)}/${bundle}`;
202
202
  }
203
- /** sha1(content) 12-hex of a bundle entry file, memoised; `Date.now()` when absent. */
204
- bundleVersionTag(bundlePath) {
205
- if (bundlePath === null) return String(Math.floor(Date.now()));
203
+ /**
204
+ * sha1 (12-hex) over EVERY file in the bundle's dist dir — content, not
205
+ * mtime (reproducible builds fix mtime, so mtime never busts). Memoised by
206
+ * the dir's (name:size:mtime) fingerprint; a redeploy that changes any
207
+ * file's content changes its size and re-hashes. `Date.now()` when the
208
+ * bundle has no local file (remote addon).
209
+ */
210
+ bundleDirVersionTag(entryPath) {
211
+ if (entryPath === null) return String(Math.floor(Date.now()));
206
212
  try {
207
- const stat = node_fs.statSync(bundlePath);
208
- const cacheKey = `${bundlePath}:${stat.size}:${stat.mtimeMs}`;
213
+ const dir = node_path.dirname(entryPath);
214
+ const files = node_fs.readdirSync(dir).filter((f) => node_fs.statSync(node_path.join(dir, f)).isFile()).sort();
215
+ const cacheKey = `${dir}#${files.map((f) => {
216
+ const s = node_fs.statSync(node_path.join(dir, f));
217
+ return `${f}:${s.size}:${s.mtimeMs}`;
218
+ }).join("|")}`;
209
219
  const cached = this.bundleHashCache.get(cacheKey);
210
220
  if (cached !== void 0) return cached;
211
- const hash = (0, node_crypto.createHash)("sha1").update(node_fs.readFileSync(bundlePath)).digest("hex").slice(0, 12);
212
- this.bundleHashCache.set(cacheKey, hash);
213
- return hash;
221
+ const hash = (0, node_crypto.createHash)("sha1");
222
+ for (const f of files) {
223
+ hash.update(f);
224
+ hash.update(node_fs.readFileSync(node_path.join(dir, f)));
225
+ }
226
+ const digest = hash.digest("hex").slice(0, 12);
227
+ this.bundleHashCache.set(cacheKey, digest);
228
+ return digest;
214
229
  } catch {
215
230
  return String(Math.floor(Date.now()));
216
231
  }
@@ -191,19 +191,34 @@ var AddonWidgetsAggregatorAddon = class extends BaseAddon {
191
191
  */
192
192
  makeBundleUrl(addonId, bundle) {
193
193
  const bundlePath = this.resolveBundlePath(addonId, bundle);
194
- return `/api/addon-widgets/${addonId}/${bundle}?v=${this.bundleVersionTag(bundlePath)}`;
194
+ return `/api/addon-widgets/${addonId}/v${this.bundleDirVersionTag(bundlePath)}/${bundle}`;
195
195
  }
196
- /** sha1(content) 12-hex of a bundle entry file, memoised; `Date.now()` when absent. */
197
- bundleVersionTag(bundlePath) {
198
- if (bundlePath === null) return String(Math.floor(Date.now()));
196
+ /**
197
+ * sha1 (12-hex) over EVERY file in the bundle's dist dir — content, not
198
+ * mtime (reproducible builds fix mtime, so mtime never busts). Memoised by
199
+ * the dir's (name:size:mtime) fingerprint; a redeploy that changes any
200
+ * file's content changes its size and re-hashes. `Date.now()` when the
201
+ * bundle has no local file (remote addon).
202
+ */
203
+ bundleDirVersionTag(entryPath) {
204
+ if (entryPath === null) return String(Math.floor(Date.now()));
199
205
  try {
200
- const stat = fs.statSync(bundlePath);
201
- const cacheKey = `${bundlePath}:${stat.size}:${stat.mtimeMs}`;
206
+ const dir = path$1.dirname(entryPath);
207
+ const files = fs.readdirSync(dir).filter((f) => fs.statSync(path$1.join(dir, f)).isFile()).sort();
208
+ const cacheKey = `${dir}#${files.map((f) => {
209
+ const s = fs.statSync(path$1.join(dir, f));
210
+ return `${f}:${s.size}:${s.mtimeMs}`;
211
+ }).join("|")}`;
202
212
  const cached = this.bundleHashCache.get(cacheKey);
203
213
  if (cached !== void 0) return cached;
204
- const hash = createHash("sha1").update(fs.readFileSync(bundlePath)).digest("hex").slice(0, 12);
205
- this.bundleHashCache.set(cacheKey, hash);
206
- return hash;
214
+ const hash = createHash("sha1");
215
+ for (const f of files) {
216
+ hash.update(f);
217
+ hash.update(fs.readFileSync(path$1.join(dir, f)));
218
+ }
219
+ const digest = hash.digest("hex").slice(0, 12);
220
+ this.bundleHashCache.set(cacheKey, digest);
221
+ return digest;
207
222
  } catch {
208
223
  return String(Math.floor(Date.now()));
209
224
  }
package/dist/index.js CHANGED
@@ -5007,6 +5007,23 @@ var CapabilityRegistry = class CapabilityRegistry {
5007
5007
  * `buildCapRouters(t, services)` contract), keeping the kernel free of any
5008
5008
  * backend coupling (self-sufficiency invariant).
5009
5009
  */
5010
+ /**
5011
+ * The mount kind a `systemOnly` method resolves through — the cap's
5012
+ * SYSTEM/wrapper provider, keyed off the cap's `mode` (singleton or
5013
+ * collection), NEVER the per-device native.
5014
+ *
5015
+ * A `systemOnly` method (declared via the `systemMethod()` factory) belongs
5016
+ * to the cap's hub-resident system/wrapper provider — e.g. a device-scoped
5017
+ * (`device-native`) cap exposing a batch/metadata call that takes `deviceIds`
5018
+ * (an ARRAY) rather than a single `deviceId`. Routing it through the cap's
5019
+ * `device-native` mount would push it into `requireDeviceScoped`, whose
5020
+ * dispatcher demands a numeric `deviceId` on EVERY method call and rejects a
5021
+ * deviceId-less input with `input must carry numeric "deviceId"`. Resolving it
5022
+ * as the cap's mode-appropriate system provider instead keeps it reachable.
5023
+ */
5024
+ function systemMountKindFor(def) {
5025
+ return def.mode === "collection" ? "collection" : "singleton";
5026
+ }
5010
5027
  function methodsFor(def) {
5011
5028
  const effective = require_dist.expandCapMethods(def);
5012
5029
  return Object.entries(effective).map(([name, schema]) => {
@@ -5065,11 +5082,13 @@ function requireLocal(capName, getLocal, services) {
5065
5082
  function buildOneCapRouter(def, primitives, services) {
5066
5083
  const capName = def.name;
5067
5084
  const mount = require_dist.resolveCapMount(def);
5068
- const isCollection = mount.kind === "collection";
5085
+ const systemMountKind = systemMountKindFor(def);
5069
5086
  const nodeIdData = def.nodeIdMode === "data";
5070
5087
  const procedures = {};
5071
- const getLocal = (ctx, addonId) => services.getLocalProvider(capName, mount.kind, ctx, addonId);
5072
5088
  for (const method of methodsFor(def)) {
5089
+ const effectiveMountKind = method.schema.systemOnly === true ? systemMountKind : mount.kind;
5090
+ const isCollection = effectiveMountKind === "collection";
5091
+ const getLocal = (ctx, addonId) => services.getLocalProvider(capName, effectiveMountKind, ctx, addonId);
5073
5092
  const base = primitives.procedures[require_dist.procedureAuthKey(method.auth)];
5074
5093
  const inputSchema = method.schema.input;
5075
5094
  if (method.kind === "subscription") {
package/dist/index.mjs CHANGED
@@ -4999,6 +4999,23 @@ var CapabilityRegistry = class CapabilityRegistry {
4999
4999
  * `buildCapRouters(t, services)` contract), keeping the kernel free of any
5000
5000
  * backend coupling (self-sufficiency invariant).
5001
5001
  */
5002
+ /**
5003
+ * The mount kind a `systemOnly` method resolves through — the cap's
5004
+ * SYSTEM/wrapper provider, keyed off the cap's `mode` (singleton or
5005
+ * collection), NEVER the per-device native.
5006
+ *
5007
+ * A `systemOnly` method (declared via the `systemMethod()` factory) belongs
5008
+ * to the cap's hub-resident system/wrapper provider — e.g. a device-scoped
5009
+ * (`device-native`) cap exposing a batch/metadata call that takes `deviceIds`
5010
+ * (an ARRAY) rather than a single `deviceId`. Routing it through the cap's
5011
+ * `device-native` mount would push it into `requireDeviceScoped`, whose
5012
+ * dispatcher demands a numeric `deviceId` on EVERY method call and rejects a
5013
+ * deviceId-less input with `input must carry numeric "deviceId"`. Resolving it
5014
+ * as the cap's mode-appropriate system provider instead keeps it reachable.
5015
+ */
5016
+ function systemMountKindFor(def) {
5017
+ return def.mode === "collection" ? "collection" : "singleton";
5018
+ }
5002
5019
  function methodsFor(def) {
5003
5020
  const effective = expandCapMethods(def);
5004
5021
  return Object.entries(effective).map(([name, schema]) => {
@@ -5057,11 +5074,13 @@ function requireLocal(capName, getLocal, services) {
5057
5074
  function buildOneCapRouter(def, primitives, services) {
5058
5075
  const capName = def.name;
5059
5076
  const mount = resolveCapMount(def);
5060
- const isCollection = mount.kind === "collection";
5077
+ const systemMountKind = systemMountKindFor(def);
5061
5078
  const nodeIdData = def.nodeIdMode === "data";
5062
5079
  const procedures = {};
5063
- const getLocal = (ctx, addonId) => services.getLocalProvider(capName, mount.kind, ctx, addonId);
5064
5080
  for (const method of methodsFor(def)) {
5081
+ const effectiveMountKind = method.schema.systemOnly === true ? systemMountKind : mount.kind;
5082
+ const isCollection = effectiveMountKind === "collection";
5083
+ const getLocal = (ctx, addonId) => services.getLocalProvider(capName, effectiveMountKind, ctx, addonId);
5065
5084
  const base = primitives.procedures[procedureAuthKey(method.auth)];
5066
5085
  const inputSchema = method.schema.input;
5067
5086
  if (method.kind === "subscription") {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/system",
3
- "version": "1.1.43",
3
+ "version": "1.1.45",
4
4
  "description": "Core addon for CamStack — builtins, pipeline, process management, auth, logging, events",
5
5
  "keywords": [
6
6
  "camstack",