@colixsystems/widget-sdk 0.73.0 → 0.75.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 +21 -1
- package/dist/contract.cjs +26 -1
- package/dist/contract.js +26 -1
- package/dist/linter.cjs +34 -0
- package/dist/linter.js +46 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -54,7 +54,27 @@ See the design reference for the full architecture: [`docs/architecture/widget-m
|
|
|
54
54
|
|
|
55
55
|
## Status
|
|
56
56
|
|
|
57
|
-
`v0.
|
|
57
|
+
`v0.75.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**.
|
|
58
|
+
|
|
59
|
+
### What's new in 0.75.0
|
|
60
|
+
|
|
61
|
+
**`ui.group: "Basics"` marks a widget's quick style knobs (sc-4100).** A `styleSchema` field has always accepted the `propertySchema` `ui.group` key; the Studio now reads one reserved group name from it, so a widget with several styleable elements can expose per-element controls without burying the two or three an author reaches for first.
|
|
62
|
+
|
|
63
|
+
- **Group every style field, or none of them.** In a grouped `styleSchema`, put the whole-widget basics (`background`, `textColor`, `align`) under the exact group `"Basics"` and each per-element field under a group named for the element it styles (`"Card"`, `"Title"`, `"Chip"`, …). Keep the total under about 12 fields.
|
|
64
|
+
- **Where each surface renders.** The Builder's Properties panel renders **every** group. Agent Mode's in-preview edit panel — a quick-tweak surface — renders **only** `"Basics"` and points the author at the Builder for the rest.
|
|
65
|
+
- **Nothing changes for an ungrouped `styleSchema`.** A flat schema (no `ui.group` anywhere) renders in full in both surfaces exactly as before, so every already-published widget is unaffected. A grouped schema that declares no `"Basics"` group also renders in full — the trim needs a group to trim *to*.
|
|
66
|
+
|
|
67
|
+
No export, type, hook, or `CONTRACT` field changed; `CONTRACT.version` stays `1.51.0`. This is an additive host convention over an existing manifest key.
|
|
68
|
+
|
|
69
|
+
### What's new in 0.74.0
|
|
70
|
+
|
|
71
|
+
**Calling a translation vendor directly is now a blocking lint finding (sc-4085).** `useTranslate()` has brokered content translation since 0.70.0, but nothing stopped a widget fetching a free public endpoint instead — and generated widgets routinely did, reaching for `api.mymemory.translated.net`. That call skips the workspace's provider, the shared cache and the metered budget, and ships an unvetted third-party endpoint into every app the widget is installed in.
|
|
72
|
+
|
|
73
|
+
- **New rule `no-external-translation-api`.** `severity: "error"`, so it fails `appstudio-widget lint` and blocks submission. There is no `appstudio-lint-ignore` opt-out: a hostname match is unambiguous, so unlike a relative `/api/` path there is no correct code for a directive to rescue.
|
|
74
|
+
- **`CONTRACT.translationApiHosts`** publishes the refused vendor list (MyMemory, LibreTranslate, DeepL, Google, Microsoft, Yandex, Lingvanex), matched case-insensitively as host substrings against code. Comments are blanked first, so documenting the rule is safe.
|
|
75
|
+
- **Nothing changes for a widget that was already correct.** `useTranslate()` for content the workspace's users typed, `useI18n().t()` for text you author, and `fetch`/`axios` for genuine third-party APIs all lint exactly as before.
|
|
76
|
+
|
|
77
|
+
`CONTRACT.version` → `1.51.0`. Additive: one new contract field and one new lint rule; no existing export changed signature.
|
|
58
78
|
|
|
59
79
|
### What's new in 0.71.0
|
|
60
80
|
|
package/dist/contract.cjs
CHANGED
|
@@ -1783,6 +1783,24 @@ const HOST_API_URL_PATTERNS = [
|
|
|
1783
1783
|
"`/api/",
|
|
1784
1784
|
];
|
|
1785
1785
|
|
|
1786
|
+
// sc-4085: machine translation is platform-brokered (`useTranslate()`), so a
|
|
1787
|
+
// widget calling a translation vendor directly is a contract violation, not a
|
|
1788
|
+
// third-party API call. The direct call bypasses the platform's provider key,
|
|
1789
|
+
// the durable translation cache, and the workspace's metered budget — the same
|
|
1790
|
+
// reason a widget may never wire its own payment processor. Matched as literal
|
|
1791
|
+
// host substrings against code (comments are blanked before matching).
|
|
1792
|
+
const TRANSLATION_API_HOSTS = [
|
|
1793
|
+
"mymemory.translated.net",
|
|
1794
|
+
"libretranslate",
|
|
1795
|
+
"deepl.com",
|
|
1796
|
+
"translate.googleapis.com",
|
|
1797
|
+
"translation.googleapis.com",
|
|
1798
|
+
"translate.google.com",
|
|
1799
|
+
"microsofttranslator.com",
|
|
1800
|
+
"translate.yandex",
|
|
1801
|
+
"lingvanex.com",
|
|
1802
|
+
];
|
|
1803
|
+
|
|
1786
1804
|
function deepFreeze(value) {
|
|
1787
1805
|
if (value === null || typeof value !== "object") return value;
|
|
1788
1806
|
if (Object.isFrozen(value)) return value;
|
|
@@ -2266,7 +2284,13 @@ const CONTRACT = deepFreeze({
|
|
|
2266
2284
|
// from this list — never told an author it existed. That omission is why
|
|
2267
2285
|
// a generated script inlines a plaintext credential instead of reading
|
|
2268
2286
|
// `secrets["KEY"]`. No runtime change; the global was already there.
|
|
2269
|
-
|
|
2287
|
+
// 1.51.0: additive (sc-4085) — `translationApiHosts`, the machine-translation
|
|
2288
|
+
// vendor hosts the linter refuses. `useTranslate()` has brokered content
|
|
2289
|
+
// translation since sc-3783, but nothing stopped a widget calling a free
|
|
2290
|
+
// public endpoint instead, which skips the cache, the metering and the
|
|
2291
|
+
// workspace's provider. Publishing the host list here keeps the linter,
|
|
2292
|
+
// the Developer guide and the agent prompt reading one source.
|
|
2293
|
+
version: "1.51.0",
|
|
2270
2294
|
sharedTranslationKeys: SHARED_TRANSLATION_KEYS,
|
|
2271
2295
|
hooks: HOOKS,
|
|
2272
2296
|
primitives: PRIMITIVES,
|
|
@@ -2287,6 +2311,7 @@ const CONTRACT = deepFreeze({
|
|
|
2287
2311
|
vettedImports: VETTED_IMPORTS,
|
|
2288
2312
|
allowedBareImports: ALLOWED_BARE_IMPORTS,
|
|
2289
2313
|
hostApiUrlPatterns: HOST_API_URL_PATTERNS,
|
|
2314
|
+
translationApiHosts: TRANSLATION_API_HOSTS,
|
|
2290
2315
|
});
|
|
2291
2316
|
|
|
2292
2317
|
function isHookAllowed(name) {
|
package/dist/contract.js
CHANGED
|
@@ -1783,6 +1783,24 @@ const HOST_API_URL_PATTERNS = [
|
|
|
1783
1783
|
"`/api/",
|
|
1784
1784
|
];
|
|
1785
1785
|
|
|
1786
|
+
// sc-4085: machine translation is platform-brokered (`useTranslate()`), so a
|
|
1787
|
+
// widget calling a translation vendor directly is a contract violation, not a
|
|
1788
|
+
// third-party API call. The direct call bypasses the platform's provider key,
|
|
1789
|
+
// the durable translation cache, and the workspace's metered budget — the same
|
|
1790
|
+
// reason a widget may never wire its own payment processor. Matched as literal
|
|
1791
|
+
// host substrings against code (comments are blanked before matching).
|
|
1792
|
+
const TRANSLATION_API_HOSTS = [
|
|
1793
|
+
"mymemory.translated.net",
|
|
1794
|
+
"libretranslate",
|
|
1795
|
+
"deepl.com",
|
|
1796
|
+
"translate.googleapis.com",
|
|
1797
|
+
"translation.googleapis.com",
|
|
1798
|
+
"translate.google.com",
|
|
1799
|
+
"microsofttranslator.com",
|
|
1800
|
+
"translate.yandex",
|
|
1801
|
+
"lingvanex.com",
|
|
1802
|
+
];
|
|
1803
|
+
|
|
1786
1804
|
function deepFreeze(value) {
|
|
1787
1805
|
if (value === null || typeof value !== "object") return value;
|
|
1788
1806
|
if (Object.isFrozen(value)) return value;
|
|
@@ -2266,7 +2284,13 @@ const CONTRACT = deepFreeze({
|
|
|
2266
2284
|
// from this list — never told an author it existed. That omission is why
|
|
2267
2285
|
// a generated script inlines a plaintext credential instead of reading
|
|
2268
2286
|
// `secrets["KEY"]`. No runtime change; the global was already there.
|
|
2269
|
-
|
|
2287
|
+
// 1.51.0: additive (sc-4085) — `translationApiHosts`, the machine-translation
|
|
2288
|
+
// vendor hosts the linter refuses. `useTranslate()` has brokered content
|
|
2289
|
+
// translation since sc-3783, but nothing stopped a widget calling a free
|
|
2290
|
+
// public endpoint instead, which skips the cache, the metering and the
|
|
2291
|
+
// workspace's provider. Publishing the host list here keeps the linter,
|
|
2292
|
+
// the Developer guide and the agent prompt reading one source.
|
|
2293
|
+
version: "1.51.0",
|
|
2270
2294
|
sharedTranslationKeys: SHARED_TRANSLATION_KEYS,
|
|
2271
2295
|
hooks: HOOKS,
|
|
2272
2296
|
primitives: PRIMITIVES,
|
|
@@ -2287,6 +2311,7 @@ const CONTRACT = deepFreeze({
|
|
|
2287
2311
|
vettedImports: VETTED_IMPORTS,
|
|
2288
2312
|
allowedBareImports: ALLOWED_BARE_IMPORTS,
|
|
2289
2313
|
hostApiUrlPatterns: HOST_API_URL_PATTERNS,
|
|
2314
|
+
translationApiHosts: TRANSLATION_API_HOSTS,
|
|
2290
2315
|
});
|
|
2291
2316
|
|
|
2292
2317
|
function isHookAllowed(name) {
|
package/dist/linter.cjs
CHANGED
|
@@ -392,6 +392,39 @@ function _hostApiUrlRules(source) {
|
|
|
392
392
|
return findings;
|
|
393
393
|
}
|
|
394
394
|
|
|
395
|
+
// sc-4085 — see linter.js for the rationale comment. The two files must stay
|
|
396
|
+
// in lockstep (the contract test asserts behaviour-equivalence).
|
|
397
|
+
function _translationApiRules(source) {
|
|
398
|
+
const findings = [];
|
|
399
|
+
const hosts = CONTRACT.translationApiHosts || [];
|
|
400
|
+
const lines = _stripNonCode(source, { keepStrings: true }).split(/\r?\n/);
|
|
401
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
402
|
+
const line = lines[i];
|
|
403
|
+
const lowered = line.toLowerCase();
|
|
404
|
+
for (const host of hosts) {
|
|
405
|
+
if (lowered.includes(host)) {
|
|
406
|
+
findings.push({
|
|
407
|
+
rule: "no-external-translation-api",
|
|
408
|
+
severity: "error",
|
|
409
|
+
label:
|
|
410
|
+
`source calls the translation vendor "${host}" directly — ` +
|
|
411
|
+
`translation is brokered by the platform. Use the SDK's ` +
|
|
412
|
+
`\`useTranslate()\` hook for content the workspace's users typed ` +
|
|
413
|
+
`(records, file names, API payloads) and \`useI18n().t(key, ` +
|
|
414
|
+
`fallback)\` for text you author yourself. A direct vendor call ` +
|
|
415
|
+
`bypasses the platform's provider key, the shared translation ` +
|
|
416
|
+
`cache and the workspace's metered budget, and it ships an ` +
|
|
417
|
+
`unvetted endpoint into the app.`,
|
|
418
|
+
line: i + 1,
|
|
419
|
+
snippet: line.trim().slice(0, 200),
|
|
420
|
+
});
|
|
421
|
+
break;
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
return findings;
|
|
426
|
+
}
|
|
427
|
+
|
|
395
428
|
// REQ-USERMGMT / REQ-ACL-SYS M3 — scope-required-for-user-mutation. See
|
|
396
429
|
// linter.js for the rationale comment. The two files must stay in
|
|
397
430
|
// lockstep (the contract test asserts behaviour-equivalence).
|
|
@@ -785,6 +818,7 @@ function lintSource(source, options) {
|
|
|
785
818
|
})),
|
|
786
819
|
);
|
|
787
820
|
findings.push(..._hostApiUrlRules(source));
|
|
821
|
+
findings.push(..._translationApiRules(source));
|
|
788
822
|
findings.push(..._lucideIconRules(source));
|
|
789
823
|
findings.push(..._reactInScopeRules(source));
|
|
790
824
|
findings.push(..._imagePercentHeightRules(source));
|
package/dist/linter.js
CHANGED
|
@@ -437,6 +437,51 @@ function _hostApiUrlRules(source) {
|
|
|
437
437
|
return findings;
|
|
438
438
|
}
|
|
439
439
|
|
|
440
|
+
// sc-4085 — no-external-translation-api.
|
|
441
|
+
//
|
|
442
|
+
// Translation is a platform capability, not a third-party API: `useTranslate()`
|
|
443
|
+
// (sc-3783) routes content through the platform's provider key, a durable
|
|
444
|
+
// tenant-scoped cache and a metered budget. A widget that fetches a vendor
|
|
445
|
+
// endpoint itself gets none of that, and ships an unvetted third-party
|
|
446
|
+
// dependency into every app the widget is installed in — the same reason a
|
|
447
|
+
// widget may never wire its own payment processor.
|
|
448
|
+
//
|
|
449
|
+
// This is an `error`, not a `warning` like `no-host-api-url`, and it carries no
|
|
450
|
+
// opt-out directive: a hostname match is unambiguous (unlike a relative `/api/`
|
|
451
|
+
// path, which a legitimate third-party call cannot always avoid), so there is
|
|
452
|
+
// no correct code for the directive to rescue. A workspace that needs a
|
|
453
|
+
// different provider changes the platform's translation key, not the widget.
|
|
454
|
+
function _translationApiRules(source) {
|
|
455
|
+
const findings = [];
|
|
456
|
+
const hosts = CONTRACT.translationApiHosts || [];
|
|
457
|
+
const lines = _stripNonCode(source, { keepStrings: true }).split(/\r?\n/);
|
|
458
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
459
|
+
const line = lines[i];
|
|
460
|
+
const lowered = line.toLowerCase();
|
|
461
|
+
for (const host of hosts) {
|
|
462
|
+
if (lowered.includes(host)) {
|
|
463
|
+
findings.push({
|
|
464
|
+
rule: "no-external-translation-api",
|
|
465
|
+
severity: "error",
|
|
466
|
+
label:
|
|
467
|
+
`source calls the translation vendor "${host}" directly — ` +
|
|
468
|
+
`translation is brokered by the platform. Use the SDK's ` +
|
|
469
|
+
`\`useTranslate()\` hook for content the workspace's users typed ` +
|
|
470
|
+
`(records, file names, API payloads) and \`useI18n().t(key, ` +
|
|
471
|
+
`fallback)\` for text you author yourself. A direct vendor call ` +
|
|
472
|
+
`bypasses the platform's provider key, the shared translation ` +
|
|
473
|
+
`cache and the workspace's metered budget, and it ships an ` +
|
|
474
|
+
`unvetted endpoint into the app.`,
|
|
475
|
+
line: i + 1,
|
|
476
|
+
snippet: line.trim().slice(0, 200),
|
|
477
|
+
});
|
|
478
|
+
break;
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
return findings;
|
|
483
|
+
}
|
|
484
|
+
|
|
440
485
|
// REQ-USERMGMT / REQ-ACL-SYS M3 — scope-required-for-user-mutation.
|
|
441
486
|
//
|
|
442
487
|
// A widget that calls `useUsers().invite()` / `.deactivate()` /
|
|
@@ -901,6 +946,7 @@ export function lintSource(source, options) {
|
|
|
901
946
|
);
|
|
902
947
|
// REQ-WSDK-PLATFORM §3.5: soft host-API URL warning (does not block).
|
|
903
948
|
findings.push(..._hostApiUrlRules(source));
|
|
949
|
+
findings.push(..._translationApiRules(source));
|
|
904
950
|
findings.push(..._lucideIconRules(source));
|
|
905
951
|
// sc-2353 — widget source must be self-contained (reference React ⇒ import it).
|
|
906
952
|
findings.push(..._reactInScopeRules(source));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@colixsystems/widget-sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.75.0",
|
|
4
4
|
"description": "Common widget interface for AppStudio. Implements WidgetManifest, WidgetContext, property schema, and helper hooks.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
],
|
|
49
49
|
"scripts": {
|
|
50
50
|
"build": "node scripts/build.js",
|
|
51
|
-
"test": "node --test src/__tests__/contract.test.js src/__tests__/hooks-users.test.js src/__tests__/hooks-groups.test.js src/__tests__/hooks-schema.test.js src/__tests__/hooks-assets-by-tag.test.js src/__tests__/hooks-filestore-upload.test.js src/__tests__/hooks-filestore-file.test.js src/__tests__/hooks-mutation.test.js src/__tests__/hooks-record-permissions.test.js src/__tests__/hooks-geolocation.test.js src/__tests__/hooks-subscription.test.js src/__tests__/hooks-volatile-query-key.test.js src/__tests__/linter-users-scope.test.js src/__tests__/linter-comments.test.js src/__tests__/linter-image-height.test.js src/__tests__/linter-platform.test.js src/__tests__/linter-react-import.test.js src/__tests__/lucide-icon-names.test.js src/__tests__/lucideIconName.test.js src/__tests__/manifest-actions.test.js src/__tests__/widget-translations.test.js src/__tests__/hooks-translate.test.js src/__tests__/devserver.test.js src/__tests__/host-externals.test.js src/__tests__/datetimepicker.test.js src/__tests__/property-schema-resolve.test.js src/__tests__/theme-components-parity.test.js src/__tests__/theme-depth-tokens.test.js"
|
|
51
|
+
"test": "node --test src/__tests__/contract.test.js src/__tests__/hooks-users.test.js src/__tests__/hooks-groups.test.js src/__tests__/hooks-schema.test.js src/__tests__/hooks-assets-by-tag.test.js src/__tests__/hooks-filestore-upload.test.js src/__tests__/hooks-filestore-file.test.js src/__tests__/hooks-mutation.test.js src/__tests__/hooks-record-permissions.test.js src/__tests__/hooks-geolocation.test.js src/__tests__/hooks-subscription.test.js src/__tests__/hooks-volatile-query-key.test.js src/__tests__/linter-users-scope.test.js src/__tests__/linter-comments.test.js src/__tests__/linter-translation-api.test.js src/__tests__/linter-image-height.test.js src/__tests__/linter-platform.test.js src/__tests__/linter-react-import.test.js src/__tests__/lucide-icon-names.test.js src/__tests__/lucideIconName.test.js src/__tests__/manifest-actions.test.js src/__tests__/widget-translations.test.js src/__tests__/hooks-translate.test.js src/__tests__/devserver.test.js src/__tests__/host-externals.test.js src/__tests__/datetimepicker.test.js src/__tests__/property-schema-resolve.test.js src/__tests__/theme-components-parity.test.js src/__tests__/theme-depth-tokens.test.js"
|
|
52
52
|
},
|
|
53
53
|
"engines": {
|
|
54
54
|
"node": ">=18"
|