@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 +12 -3
- package/dist/contract.cjs +39 -26
- package/dist/contract.js +39 -26
- package/dist/linter.cjs +31 -0
- package/dist/linter.js +31 -0
- package/package.json +1 -1
- package/src/contract.js +39 -26
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.
|
|
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 }
|
|
5125
|
-
//
|
|
5126
|
-
//
|
|
5127
|
-
//
|
|
5128
|
-
// the
|
|
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
|
-
|
|
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 }
|
|
5122
|
-
//
|
|
5123
|
-
//
|
|
5124
|
-
//
|
|
5125
|
-
// the
|
|
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
|
-
|
|
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.
|
|
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 }
|
|
5122
|
-
//
|
|
5123
|
-
//
|
|
5124
|
-
//
|
|
5125
|
-
// the
|
|
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
|
-
|
|
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) {
|