@colixsystems/widget-sdk 0.73.0 → 0.74.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
@@ -54,7 +54,17 @@ See the design reference for the full architecture: [`docs/architecture/widget-m
54
54
 
55
55
  ## Status
56
56
 
57
- `v0.73.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**.
57
+ `v0.74.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.74.0
60
+
61
+ **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.
62
+
63
+ - **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.
64
+ - **`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.
65
+ - **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.
66
+
67
+ `CONTRACT.version` → `1.51.0`. Additive: one new contract field and one new lint rule; no existing export changed signature.
58
68
 
59
69
  ### What's new in 0.71.0
60
70
 
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
- version: "1.50.0",
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
- version: "1.50.0",
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.73.0",
3
+ "version": "0.74.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"