@colixsystems/widget-sdk 0.272.0 → 0.274.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,15 @@ 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.274.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.274.0 (contract 1.224.0)
92
+
93
+ **The shared widget strings ship in Swedish.** Every entry in `CONTRACT.sharedTranslationKeys` now carries a hand-written `sv` value beside `en` (`cancel` is `{ en: "Cancel", sv: "Avbryt" }`), so a Swedish workspace gets the right word instead of a machine translation of a lone word. Translation seeding also matches a regional language to its primary one: a workspace language `sv-SE` takes the `sv` text a widget's `manifest.translations` ships, unless the widget ships `sv-SE` itself.
94
+
95
+ ### What's new in 0.273.0 (contract 1.223.0)
96
+
97
+ **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
98
 
91
99
  ### What's new in 0.272.0 (contract 1.222.0)
92
100
 
@@ -3628,7 +3636,7 @@ export default function OpenTasks({ tableId, titleField, statusField }) {
3628
3636
  | `filter` | Keeps the rows that match. A map `{ column: "op:value" }` (an array of expressions puts a range on one column), or an array `[{ column, operator, value }]`. |
3629
3637
  | `filterMode` | `"and"` (the default) keeps rows matching every condition; `"or"` keeps rows matching any. |
3630
3638
  | `q` | Free-text search: a case-insensitive substring match across every text column of the table (encrypted columns excepted). |
3631
- | `sort` | `{ field, dir }` with `dir` `"asc"` or `"desc"`, or the string `"field:desc"`. One column; `created_at` and `updated_at` work too. |
3639
+ | `sort` | `{ field, dir }` with `dir` `"asc"` or `"desc"`, or the string `"field:desc"`. One column; `created_at` and `updated_at` work too. Newest first is `{ field: "created_at", dir: "desc" }`. `"-field"` or an unknown column is refused: `data` stays empty and `error` names the sortable columns. |
3632
3640
  | `limit` | Rows per page: 50 when left out, at most 1000. |
3633
3641
  | `offset` | Rows to skip, for the next page. |
3634
3642
  | `includeCapabilities` | `true` stamps each row with `_capabilities`, what the signed-in user may do to it. |
@@ -3661,12 +3669,13 @@ export default function ContactList({ tableId }) {
3661
3669
  limit: PAGE,
3662
3670
  offset: page * PAGE,
3663
3671
  }));
3664
- const { data, total, loading } = useDatastoreQuery(tableId, query);
3672
+ const { data, total, loading, error } = useDatastoreQuery(tableId, query);
3665
3673
  const pick = (next) => { setStatus(next); setPage(0); };
3666
3674
  return (
3667
3675
  <View>
3668
3676
  <TextInput value={draft} onChangeText={setDraft} placeholder="Search" />
3669
3677
  <Pressable onPress={() => pick(status === "Won" ? "" : "Won")}><Text>Won</Text></Pressable>
3678
+ {error ? <Text>{error.message}</Text> : null}
3670
3679
  {loading ? <Text>Loading…</Text> : data.map((row) => <Text key={row.id}>{row.name}</Text>)}
3671
3680
  <Text>{total ?? 0} contacts</Text>
3672
3681
  {(page + 1) * PAGE < (total ?? 0) ? (
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
@@ -5121,32 +5127,33 @@ function sharedTranslationKey(key) {
5121
5127
  return `${SHARED_TRANSLATION_PREFIX}${key}`;
5122
5128
  }
5123
5129
 
5124
- // The predefined set. Map of bare key -> { en } (English default only; the
5125
- // tenant adds target-language values once in the Translations admin and every
5126
- // widget inherits them). Adding/removing a key is a CONTRACT change (it grows
5127
- // the dictionary every tenant gets), so the list lives with the contract and
5128
- // the seeder reads it from here — never a second copy.
5130
+ // The predefined set. Map of bare key -> { en, sv }. Swedish is shipped by
5131
+ // hand because a lone word machine-translates to the wrong sense ("Cancel" ->
5132
+ // "Avboka"); other languages are translated in the Translations admin and
5133
+ // every widget inherits them. Adding/removing a key is a CONTRACT change (it
5134
+ // grows the dictionary every tenant gets), so the list lives with the contract
5135
+ // and the seeder reads it from here — never a second copy.
5129
5136
  const SHARED_TRANSLATION_KEYS = Object.freeze({
5130
- submit: { en: "Submit" },
5131
- cancel: { en: "Cancel" },
5132
- save: { en: "Save" },
5133
- delete: { en: "Delete" },
5134
- edit: { en: "Edit" },
5135
- close: { en: "Close" },
5136
- confirm: { en: "Confirm" },
5137
- back: { en: "Back" },
5138
- next: { en: "Next" },
5139
- previous: { en: "Previous" },
5140
- yes: { en: "Yes" },
5141
- no: { en: "No" },
5142
- ok: { en: "OK" },
5143
- loading: { en: "Loading…" },
5144
- search: { en: "Search" },
5145
- no_results: { en: "No results" },
5146
- required: { en: "Required" },
5147
- retry: { en: "Retry" },
5148
- add: { en: "Add" },
5149
- remove: { en: "Remove" },
5137
+ submit: { en: "Submit", sv: "Skicka" },
5138
+ cancel: { en: "Cancel", sv: "Avbryt" },
5139
+ save: { en: "Save", sv: "Spara" },
5140
+ delete: { en: "Delete", sv: "Ta bort" },
5141
+ edit: { en: "Edit", sv: "Redigera" },
5142
+ close: { en: "Close", sv: "Stäng" },
5143
+ confirm: { en: "Confirm", sv: "Bekräfta" },
5144
+ back: { en: "Back", sv: "Tillbaka" },
5145
+ next: { en: "Next", sv: "Nästa" },
5146
+ previous: { en: "Previous", sv: "Föregående" },
5147
+ yes: { en: "Yes", sv: "Ja" },
5148
+ no: { en: "No", sv: "Nej" },
5149
+ ok: { en: "OK", sv: "OK" },
5150
+ loading: { en: "Loading…", sv: "Laddar…" },
5151
+ search: { en: "Search", sv: "Sök" },
5152
+ no_results: { en: "No results", sv: "Inga resultat" },
5153
+ required: { en: "Required", sv: "Obligatoriskt" },
5154
+ retry: { en: "Retry", sv: "Försök igen" },
5155
+ add: { en: "Add", sv: "Lägg till" },
5156
+ remove: { en: "Remove", sv: "Ta bort" },
5150
5157
  });
5151
5158
 
5152
5159
  // True when `key` is one of the predefined shared keys — the discriminator
@@ -6559,7 +6566,12 @@ const CONTRACT = deepFreeze({
6559
6566
  // 1.222.0: stricter (sc-14479) — `self`, `top`, `parent`, `frames`,
6560
6567
  // `opener`, `frameElement` and `.constructor` join bannedApis; `eval` and
6561
6568
  // `Function` are refused as any reference, not only as a call.
6562
- version: "1.222.0",
6569
+ // 1.223.0: stricter (sc-12906) — `scopeMintPatterns`; a widget that calls
6570
+ // the scope-header mint or sets `X-Widget-Scopes` is refused
6571
+ // (`no-scope-header-mint`).
6572
+ // 1.224.0: additive (sc-14501) — every `sharedTranslationKeys` entry
6573
+ // ships a hand-written `sv` value beside `en`.
6574
+ version: "1.224.0",
6563
6575
  sharedTranslationKeys: SHARED_TRANSLATION_KEYS,
6564
6576
  hooks: HOOKS,
6565
6577
  primitives: PRIMITIVES,
@@ -6604,6 +6616,7 @@ const CONTRACT = deepFreeze({
6604
6616
  currencyFormats: CURRENCY_FORMATS,
6605
6617
  currencyFormatFallback: CURRENCY_FORMAT_FALLBACK,
6606
6618
  translationApiHosts: TRANSLATION_API_HOSTS,
6619
+ scopeMintPatterns: SCOPE_MINT_PATTERNS,
6607
6620
  });
6608
6621
 
6609
6622
  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
@@ -5118,32 +5124,33 @@ function sharedTranslationKey(key) {
5118
5124
  return `${SHARED_TRANSLATION_PREFIX}${key}`;
5119
5125
  }
5120
5126
 
5121
- // The predefined set. Map of bare key -> { en } (English default only; the
5122
- // tenant adds target-language values once in the Translations admin and every
5123
- // widget inherits them). Adding/removing a key is a CONTRACT change (it grows
5124
- // the dictionary every tenant gets), so the list lives with the contract and
5125
- // the seeder reads it from here — never a second copy.
5127
+ // The predefined set. Map of bare key -> { en, sv }. Swedish is shipped by
5128
+ // hand because a lone word machine-translates to the wrong sense ("Cancel" ->
5129
+ // "Avboka"); other languages are translated in the Translations admin and
5130
+ // every widget inherits them. Adding/removing a key is a CONTRACT change (it
5131
+ // grows the dictionary every tenant gets), so the list lives with the contract
5132
+ // and the seeder reads it from here — never a second copy.
5126
5133
  const SHARED_TRANSLATION_KEYS = Object.freeze({
5127
- submit: { en: "Submit" },
5128
- cancel: { en: "Cancel" },
5129
- save: { en: "Save" },
5130
- delete: { en: "Delete" },
5131
- edit: { en: "Edit" },
5132
- close: { en: "Close" },
5133
- confirm: { en: "Confirm" },
5134
- back: { en: "Back" },
5135
- next: { en: "Next" },
5136
- previous: { en: "Previous" },
5137
- yes: { en: "Yes" },
5138
- no: { en: "No" },
5139
- ok: { en: "OK" },
5140
- loading: { en: "Loading…" },
5141
- search: { en: "Search" },
5142
- no_results: { en: "No results" },
5143
- required: { en: "Required" },
5144
- retry: { en: "Retry" },
5145
- add: { en: "Add" },
5146
- remove: { en: "Remove" },
5134
+ submit: { en: "Submit", sv: "Skicka" },
5135
+ cancel: { en: "Cancel", sv: "Avbryt" },
5136
+ save: { en: "Save", sv: "Spara" },
5137
+ delete: { en: "Delete", sv: "Ta bort" },
5138
+ edit: { en: "Edit", sv: "Redigera" },
5139
+ close: { en: "Close", sv: "Stäng" },
5140
+ confirm: { en: "Confirm", sv: "Bekräfta" },
5141
+ back: { en: "Back", sv: "Tillbaka" },
5142
+ next: { en: "Next", sv: "Nästa" },
5143
+ previous: { en: "Previous", sv: "Föregående" },
5144
+ yes: { en: "Yes", sv: "Ja" },
5145
+ no: { en: "No", sv: "Nej" },
5146
+ ok: { en: "OK", sv: "OK" },
5147
+ loading: { en: "Loading…", sv: "Laddar…" },
5148
+ search: { en: "Search", sv: "Sök" },
5149
+ no_results: { en: "No results", sv: "Inga resultat" },
5150
+ required: { en: "Required", sv: "Obligatoriskt" },
5151
+ retry: { en: "Retry", sv: "Försök igen" },
5152
+ add: { en: "Add", sv: "Lägg till" },
5153
+ remove: { en: "Remove", sv: "Ta bort" },
5147
5154
  });
5148
5155
 
5149
5156
  // True when `key` is one of the predefined shared keys — the discriminator
@@ -6556,7 +6563,12 @@ const CONTRACT = deepFreeze({
6556
6563
  // 1.222.0: stricter (sc-14479) — `self`, `top`, `parent`, `frames`,
6557
6564
  // `opener`, `frameElement` and `.constructor` join bannedApis; `eval` and
6558
6565
  // `Function` are refused as any reference, not only as a call.
6559
- version: "1.222.0",
6566
+ // 1.223.0: stricter (sc-12906) — `scopeMintPatterns`; a widget that calls
6567
+ // the scope-header mint or sets `X-Widget-Scopes` is refused
6568
+ // (`no-scope-header-mint`).
6569
+ // 1.224.0: additive (sc-14501) — every `sharedTranslationKeys` entry
6570
+ // ships a hand-written `sv` value beside `en`.
6571
+ version: "1.224.0",
6560
6572
  sharedTranslationKeys: SHARED_TRANSLATION_KEYS,
6561
6573
  hooks: HOOKS,
6562
6574
  primitives: PRIMITIVES,
@@ -6601,6 +6613,7 @@ const CONTRACT = deepFreeze({
6601
6613
  currencyFormats: CURRENCY_FORMATS,
6602
6614
  currencyFormatFallback: CURRENCY_FORMAT_FALLBACK,
6603
6615
  translationApiHosts: TRANSLATION_API_HOSTS,
6616
+ scopeMintPatterns: SCOPE_MINT_PATTERNS,
6604
6617
  });
6605
6618
 
6606
6619
  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.274.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
@@ -5118,32 +5124,33 @@ function sharedTranslationKey(key) {
5118
5124
  return `${SHARED_TRANSLATION_PREFIX}${key}`;
5119
5125
  }
5120
5126
 
5121
- // The predefined set. Map of bare key -> { en } (English default only; the
5122
- // tenant adds target-language values once in the Translations admin and every
5123
- // widget inherits them). Adding/removing a key is a CONTRACT change (it grows
5124
- // the dictionary every tenant gets), so the list lives with the contract and
5125
- // the seeder reads it from here — never a second copy.
5127
+ // The predefined set. Map of bare key -> { en, sv }. Swedish is shipped by
5128
+ // hand because a lone word machine-translates to the wrong sense ("Cancel" ->
5129
+ // "Avboka"); other languages are translated in the Translations admin and
5130
+ // every widget inherits them. Adding/removing a key is a CONTRACT change (it
5131
+ // grows the dictionary every tenant gets), so the list lives with the contract
5132
+ // and the seeder reads it from here — never a second copy.
5126
5133
  const SHARED_TRANSLATION_KEYS = Object.freeze({
5127
- submit: { en: "Submit" },
5128
- cancel: { en: "Cancel" },
5129
- save: { en: "Save" },
5130
- delete: { en: "Delete" },
5131
- edit: { en: "Edit" },
5132
- close: { en: "Close" },
5133
- confirm: { en: "Confirm" },
5134
- back: { en: "Back" },
5135
- next: { en: "Next" },
5136
- previous: { en: "Previous" },
5137
- yes: { en: "Yes" },
5138
- no: { en: "No" },
5139
- ok: { en: "OK" },
5140
- loading: { en: "Loading…" },
5141
- search: { en: "Search" },
5142
- no_results: { en: "No results" },
5143
- required: { en: "Required" },
5144
- retry: { en: "Retry" },
5145
- add: { en: "Add" },
5146
- remove: { en: "Remove" },
5134
+ submit: { en: "Submit", sv: "Skicka" },
5135
+ cancel: { en: "Cancel", sv: "Avbryt" },
5136
+ save: { en: "Save", sv: "Spara" },
5137
+ delete: { en: "Delete", sv: "Ta bort" },
5138
+ edit: { en: "Edit", sv: "Redigera" },
5139
+ close: { en: "Close", sv: "Stäng" },
5140
+ confirm: { en: "Confirm", sv: "Bekräfta" },
5141
+ back: { en: "Back", sv: "Tillbaka" },
5142
+ next: { en: "Next", sv: "Nästa" },
5143
+ previous: { en: "Previous", sv: "Föregående" },
5144
+ yes: { en: "Yes", sv: "Ja" },
5145
+ no: { en: "No", sv: "Nej" },
5146
+ ok: { en: "OK", sv: "OK" },
5147
+ loading: { en: "Loading…", sv: "Laddar…" },
5148
+ search: { en: "Search", sv: "Sök" },
5149
+ no_results: { en: "No results", sv: "Inga resultat" },
5150
+ required: { en: "Required", sv: "Obligatoriskt" },
5151
+ retry: { en: "Retry", sv: "Försök igen" },
5152
+ add: { en: "Add", sv: "Lägg till" },
5153
+ remove: { en: "Remove", sv: "Ta bort" },
5147
5154
  });
5148
5155
 
5149
5156
  // True when `key` is one of the predefined shared keys — the discriminator
@@ -6556,7 +6563,12 @@ const CONTRACT = deepFreeze({
6556
6563
  // 1.222.0: stricter (sc-14479) — `self`, `top`, `parent`, `frames`,
6557
6564
  // `opener`, `frameElement` and `.constructor` join bannedApis; `eval` and
6558
6565
  // `Function` are refused as any reference, not only as a call.
6559
- version: "1.222.0",
6566
+ // 1.223.0: stricter (sc-12906) — `scopeMintPatterns`; a widget that calls
6567
+ // the scope-header mint or sets `X-Widget-Scopes` is refused
6568
+ // (`no-scope-header-mint`).
6569
+ // 1.224.0: additive (sc-14501) — every `sharedTranslationKeys` entry
6570
+ // ships a hand-written `sv` value beside `en`.
6571
+ version: "1.224.0",
6560
6572
  sharedTranslationKeys: SHARED_TRANSLATION_KEYS,
6561
6573
  hooks: HOOKS,
6562
6574
  primitives: PRIMITIVES,
@@ -6601,6 +6613,7 @@ const CONTRACT = deepFreeze({
6601
6613
  currencyFormats: CURRENCY_FORMATS,
6602
6614
  currencyFormatFallback: CURRENCY_FORMAT_FALLBACK,
6603
6615
  translationApiHosts: TRANSLATION_API_HOSTS,
6616
+ scopeMintPatterns: SCOPE_MINT_PATTERNS,
6604
6617
  });
6605
6618
 
6606
6619
  function isHookAllowed(name) {