@colixsystems/widget-sdk 0.272.0 → 0.273.0

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/README.md CHANGED
@@ -86,7 +86,11 @@ See the design reference for the full architecture: [`docs/architecture/widget-m
86
86
 
87
87
  ## Status
88
88
 
89
- `v0.272.0` — pre-publish. The package surface (types, function names, export paths) is the v1 contract; runtime behaviour for some hooks is stubbed (each hook documents what's wired and what isn't). It is **not yet published to npm**.
89
+ `v0.273.0` — pre-publish. The package surface (types, function names, export paths) is the v1 contract; runtime behaviour for some hooks is stubbed (each hook documents what's wired and what isn't). It is **not yet published to npm**.
90
+
91
+ ### What's new in 0.273.0 (contract 1.223.0)
92
+
93
+ **A widget never mints or sets its own permission header.** The host attaches each widget's `X-Widget-Scopes` header itself, from what that widget was granted. Source that calls `/widgets/scope-token` or sets `X-Widget-Scopes` (any casing) is now a blocking `no-scope-header-mint` finding, listed in the new `CONTRACT.scopeMintPatterns`. Comments are not scanned. Declare the scopes your hooks need in `manifest.requestedScopes`, as before; a widget that does that is unaffected.
90
94
 
91
95
  ### What's new in 0.272.0 (contract 1.222.0)
92
96
 
package/dist/contract.cjs CHANGED
@@ -4956,6 +4956,12 @@ const TRANSLATION_API_HOSTS = [
4956
4956
  "lingvanex.com",
4957
4957
  ];
4958
4958
 
4959
+ // sc-12906: the host mints a widget's `X-Widget-Scopes` header, never the
4960
+ // widget. Bundles share the host's JS realm, so a widget calling the mint
4961
+ // itself, or forging the header, could borrow another widget's scopes. Matched
4962
+ // case-insensitively against code (comments are blanked before matching).
4963
+ const SCOPE_MINT_PATTERNS = ["/widgets/scope-token", "x-widget-scopes"];
4964
+
4959
4965
  // sc-4686 — the currency a price renders in when the HOST does not supply the
4960
4966
  // workspace's own (a host predating the workspace.currency slice, or a caller
4961
4967
  // that forgot to wire it). A rendering fallback so a price is never blank or
@@ -6559,7 +6565,10 @@ const CONTRACT = deepFreeze({
6559
6565
  // 1.222.0: stricter (sc-14479) — `self`, `top`, `parent`, `frames`,
6560
6566
  // `opener`, `frameElement` and `.constructor` join bannedApis; `eval` and
6561
6567
  // `Function` are refused as any reference, not only as a call.
6562
- version: "1.222.0",
6568
+ // 1.223.0: stricter (sc-12906) — `scopeMintPatterns`; a widget that calls
6569
+ // the scope-header mint or sets `X-Widget-Scopes` is refused
6570
+ // (`no-scope-header-mint`).
6571
+ version: "1.223.0",
6563
6572
  sharedTranslationKeys: SHARED_TRANSLATION_KEYS,
6564
6573
  hooks: HOOKS,
6565
6574
  primitives: PRIMITIVES,
@@ -6604,6 +6613,7 @@ const CONTRACT = deepFreeze({
6604
6613
  currencyFormats: CURRENCY_FORMATS,
6605
6614
  currencyFormatFallback: CURRENCY_FORMAT_FALLBACK,
6606
6615
  translationApiHosts: TRANSLATION_API_HOSTS,
6616
+ scopeMintPatterns: SCOPE_MINT_PATTERNS,
6607
6617
  });
6608
6618
 
6609
6619
  function isHookAllowed(name) {
package/dist/contract.js CHANGED
@@ -4953,6 +4953,12 @@ const TRANSLATION_API_HOSTS = [
4953
4953
  "lingvanex.com",
4954
4954
  ];
4955
4955
 
4956
+ // sc-12906: the host mints a widget's `X-Widget-Scopes` header, never the
4957
+ // widget. Bundles share the host's JS realm, so a widget calling the mint
4958
+ // itself, or forging the header, could borrow another widget's scopes. Matched
4959
+ // case-insensitively against code (comments are blanked before matching).
4960
+ const SCOPE_MINT_PATTERNS = ["/widgets/scope-token", "x-widget-scopes"];
4961
+
4956
4962
  // sc-4686 — the currency a price renders in when the HOST does not supply the
4957
4963
  // workspace's own (a host predating the workspace.currency slice, or a caller
4958
4964
  // that forgot to wire it). A rendering fallback so a price is never blank or
@@ -6556,7 +6562,10 @@ const CONTRACT = deepFreeze({
6556
6562
  // 1.222.0: stricter (sc-14479) — `self`, `top`, `parent`, `frames`,
6557
6563
  // `opener`, `frameElement` and `.constructor` join bannedApis; `eval` and
6558
6564
  // `Function` are refused as any reference, not only as a call.
6559
- version: "1.222.0",
6565
+ // 1.223.0: stricter (sc-12906) — `scopeMintPatterns`; a widget that calls
6566
+ // the scope-header mint or sets `X-Widget-Scopes` is refused
6567
+ // (`no-scope-header-mint`).
6568
+ version: "1.223.0",
6560
6569
  sharedTranslationKeys: SHARED_TRANSLATION_KEYS,
6561
6570
  hooks: HOOKS,
6562
6571
  primitives: PRIMITIVES,
@@ -6601,6 +6610,7 @@ const CONTRACT = deepFreeze({
6601
6610
  currencyFormats: CURRENCY_FORMATS,
6602
6611
  currencyFormatFallback: CURRENCY_FORMAT_FALLBACK,
6603
6612
  translationApiHosts: TRANSLATION_API_HOSTS,
6613
+ scopeMintPatterns: SCOPE_MINT_PATTERNS,
6604
6614
  });
6605
6615
 
6606
6616
  function isHookAllowed(name) {
package/dist/linter.cjs CHANGED
@@ -763,6 +763,36 @@ function _translationApiRules(source) {
763
763
  return findings;
764
764
  }
765
765
 
766
+ // sc-12906 — no-scope-header-mint.
767
+ //
768
+ // The host mints each widget's `X-Widget-Scopes` header from the widget's own
769
+ // installation. A bundle runs in the host's JS realm, so one that calls the
770
+ // mint itself, or sets the header, could borrow another widget's scopes. The
771
+ // server refuses a built-in nobody placed; this catches the attempt at publish.
772
+ // An error with no opt-out: no correct widget touches either.
773
+ function _scopeMintRules(source) {
774
+ const findings = [];
775
+ const needles = CONTRACT.scopeMintPatterns || [];
776
+ const lines = _stripNonCode(source, { keepStrings: true }).split(/\r?\n/);
777
+ for (let i = 0; i < lines.length; i += 1) {
778
+ const lowered = lines[i].toLowerCase();
779
+ const needle = needles.find((n) => lowered.includes(n));
780
+ if (!needle) continue;
781
+ findings.push({
782
+ rule: "no-scope-header-mint",
783
+ severity: "error",
784
+ label:
785
+ `source references "${needle}" — the host attaches each widget's ` +
786
+ `permission header itself. Never call that endpoint or set that ` +
787
+ `header; declare the scopes your hooks need in ` +
788
+ `manifest.requestedScopes.`,
789
+ line: i + 1,
790
+ snippet: lines[i].trim().slice(0, 200),
791
+ });
792
+ }
793
+ return findings;
794
+ }
795
+
766
796
  // REQ-USERMGMT / REQ-ACL-SYS M3 — scope-required-for-user-mutation. See
767
797
  // linter.js for the rationale comment. The two files must stay in
768
798
  // lockstep (the contract test asserts behaviour-equivalence).
@@ -2460,6 +2490,7 @@ function lintSource(source, options) {
2460
2490
  // sc-6970 — soft warning: HTML markup spliced into a content string.
2461
2491
  findings.push(..._htmlInContentRules(source));
2462
2492
  findings.push(..._translationApiRules(source));
2493
+ findings.push(..._scopeMintRules(source));
2463
2494
  findings.push(..._handBuiltPageUrlRules(source));
2464
2495
  findings.push(..._lucideIconRules(source));
2465
2496
  findings.push(..._bundledAssetRules(source, options && options.assetFiles));
package/dist/linter.js CHANGED
@@ -695,6 +695,36 @@ function _translationApiRules(source) {
695
695
  return findings;
696
696
  }
697
697
 
698
+ // sc-12906 — no-scope-header-mint.
699
+ //
700
+ // The host mints each widget's `X-Widget-Scopes` header from the widget's own
701
+ // installation. A bundle runs in the host's JS realm, so one that calls the
702
+ // mint itself, or sets the header, could borrow another widget's scopes. The
703
+ // server refuses a built-in nobody placed; this catches the attempt at publish.
704
+ // An error with no opt-out: no correct widget touches either.
705
+ function _scopeMintRules(source) {
706
+ const findings = [];
707
+ const needles = CONTRACT.scopeMintPatterns || [];
708
+ const lines = _stripNonCode(source, { keepStrings: true }).split(/\r?\n/);
709
+ for (let i = 0; i < lines.length; i += 1) {
710
+ const lowered = lines[i].toLowerCase();
711
+ const needle = needles.find((n) => lowered.includes(n));
712
+ if (!needle) continue;
713
+ findings.push({
714
+ rule: "no-scope-header-mint",
715
+ severity: "error",
716
+ label:
717
+ `source references "${needle}" — the host attaches each widget's ` +
718
+ `permission header itself. Never call that endpoint or set that ` +
719
+ `header; declare the scopes your hooks need in ` +
720
+ `manifest.requestedScopes.`,
721
+ line: i + 1,
722
+ snippet: lines[i].trim().slice(0, 200),
723
+ });
724
+ }
725
+ return findings;
726
+ }
727
+
698
728
  // REQ-USERMGMT / REQ-ACL-SYS M3 — scope-required-for-user-mutation.
699
729
  //
700
730
  // A widget that calls `useUsers().invite()` / `.deactivate()` /
@@ -2520,6 +2550,7 @@ export function lintSource(source, options) {
2520
2550
  // sc-6970 — soft warning: HTML markup spliced into a content string.
2521
2551
  findings.push(..._htmlInContentRules(source));
2522
2552
  findings.push(..._translationApiRules(source));
2553
+ findings.push(..._scopeMintRules(source));
2523
2554
  findings.push(..._handBuiltPageUrlRules(source));
2524
2555
  findings.push(..._lucideIconRules(source));
2525
2556
  findings.push(..._bundledAssetRules(source, options && options.assetFiles));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@colixsystems/widget-sdk",
3
- "version": "0.272.0",
3
+ "version": "0.273.0",
4
4
  "description": "Common widget interface for AppStudio. Implements WidgetManifest, WidgetContext, property schema, and helper hooks.",
5
5
  "homepage": "https://github.com/Colix-AB/AppStudio",
6
6
  "type": "module",
package/src/contract.js CHANGED
@@ -4953,6 +4953,12 @@ const TRANSLATION_API_HOSTS = [
4953
4953
  "lingvanex.com",
4954
4954
  ];
4955
4955
 
4956
+ // sc-12906: the host mints a widget's `X-Widget-Scopes` header, never the
4957
+ // widget. Bundles share the host's JS realm, so a widget calling the mint
4958
+ // itself, or forging the header, could borrow another widget's scopes. Matched
4959
+ // case-insensitively against code (comments are blanked before matching).
4960
+ const SCOPE_MINT_PATTERNS = ["/widgets/scope-token", "x-widget-scopes"];
4961
+
4956
4962
  // sc-4686 — the currency a price renders in when the HOST does not supply the
4957
4963
  // workspace's own (a host predating the workspace.currency slice, or a caller
4958
4964
  // that forgot to wire it). A rendering fallback so a price is never blank or
@@ -6556,7 +6562,10 @@ const CONTRACT = deepFreeze({
6556
6562
  // 1.222.0: stricter (sc-14479) — `self`, `top`, `parent`, `frames`,
6557
6563
  // `opener`, `frameElement` and `.constructor` join bannedApis; `eval` and
6558
6564
  // `Function` are refused as any reference, not only as a call.
6559
- version: "1.222.0",
6565
+ // 1.223.0: stricter (sc-12906) — `scopeMintPatterns`; a widget that calls
6566
+ // the scope-header mint or sets `X-Widget-Scopes` is refused
6567
+ // (`no-scope-header-mint`).
6568
+ version: "1.223.0",
6560
6569
  sharedTranslationKeys: SHARED_TRANSLATION_KEYS,
6561
6570
  hooks: HOOKS,
6562
6571
  primitives: PRIMITIVES,
@@ -6601,6 +6610,7 @@ const CONTRACT = deepFreeze({
6601
6610
  currencyFormats: CURRENCY_FORMATS,
6602
6611
  currencyFormatFallback: CURRENCY_FORMAT_FALLBACK,
6603
6612
  translationApiHosts: TRANSLATION_API_HOSTS,
6613
+ scopeMintPatterns: SCOPE_MINT_PATTERNS,
6604
6614
  });
6605
6615
 
6606
6616
  function isHookAllowed(name) {