@colixsystems/widget-sdk 0.88.0 → 0.89.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 +27 -1
- package/dist/linter.cjs +44 -0
- package/dist/linter.js +44 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -61,7 +61,33 @@ See the design reference for the full architecture: [`docs/architecture/widget-m
|
|
|
61
61
|
|
|
62
62
|
## Status
|
|
63
63
|
|
|
64
|
-
`v0.
|
|
64
|
+
`v0.89.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**.
|
|
65
|
+
|
|
66
|
+
### What's new in 0.89.0 (contract unchanged)
|
|
67
|
+
|
|
68
|
+
**New linter rule `write-not-gated-on-user` — a widget that writes must decide what a signed-OUT visitor sees (sc-4985).**
|
|
69
|
+
|
|
70
|
+
- **`write-not-gated-on-user` (severity `warning`, non-blocking).** A widget that
|
|
71
|
+
writes with `useDatastoreMutation` but carries no identity guard is flagged. A
|
|
72
|
+
write needs a signed-in app user, so an anonymous visitor handed a live
|
|
73
|
+
"Save" / "Book" / "Delete" button can only ever tap it and fail. Author fix:
|
|
74
|
+
read `useUser()` and branch **before** rendering the control — when `!user.id`
|
|
75
|
+
keep the affordance as a visibly-inactive signpost with a translated "sign in"
|
|
76
|
+
line and **no press handler** (a widget cannot open the login surface; that is
|
|
77
|
+
a built-in Button's `sign-in` action, wired by the page author), and when the
|
|
78
|
+
user is signed in but not permitted, leave the control out entirely.
|
|
79
|
+
- **Reading `useUser().id` as a VALUE does not satisfy it.** The canonical
|
|
80
|
+
USER-column write pattern (`create({ [memberField]: user.id })`) calls
|
|
81
|
+
`useUser()` without ever branching on it — the case most easily mistaken for a
|
|
82
|
+
gate — so the rule requires an operator after `.id` (a negation, a ternary,
|
|
83
|
+
`&&`, a comparison) or a `groupIds` / `roles` check.
|
|
84
|
+
- **Why a warning.** It is conservative on purpose: an unrelated `.id`
|
|
85
|
+
comparison elsewhere in the source silences it. A rule that occasionally stays
|
|
86
|
+
quiet is far cheaper than one that cries wolf on correct code, and gating is an
|
|
87
|
+
affordance decision — the server remains the only authority, so the `catch`
|
|
88
|
+
stays either way.
|
|
89
|
+
|
|
90
|
+
`CONTRACT` is unchanged (no new field), and no export changed signature.
|
|
65
91
|
|
|
66
92
|
### What's new in 0.88.0 (contract 1.62.0)
|
|
67
93
|
|
package/dist/linter.cjs
CHANGED
|
@@ -789,6 +789,49 @@ const CURRENCY_LABEL_RES = [
|
|
|
789
789
|
/(?:\d|\})\s*\b(?:SEK|NOK|DKK|EUR|GBP|USD|CHF|PLN|CZK|HUF|JPY|INR)\b/,
|
|
790
790
|
];
|
|
791
791
|
|
|
792
|
+
// sc-4985 — soft warning: a widget that writes must decide what a signed-OUT
|
|
793
|
+
// visitor sees. A write needs a signed-in app user, so an anonymous visitor
|
|
794
|
+
// handed a live "Save" / "Book" / "Delete" button can only tap it and fail —
|
|
795
|
+
// the failure the gate exists to spare them. Satisfied by any identity guard:
|
|
796
|
+
// a negated or compared `.id`, or a `groupIds` / `roles` check. Reading
|
|
797
|
+
// `useUser().id` purely as a VALUE (the USER-column write pattern) is NOT a
|
|
798
|
+
// guard, which is why an operator has to follow it.
|
|
799
|
+
//
|
|
800
|
+
// Conservative on purpose: an unrelated `.id` comparison elsewhere in the
|
|
801
|
+
// source silences the rule. A warning that occasionally stays quiet is far
|
|
802
|
+
// cheaper than one that cries wolf on correct code.
|
|
803
|
+
const _IDENTITY_GUARD_RES = [
|
|
804
|
+
// `!user.id` / `!user?.id`
|
|
805
|
+
/![\s(]*\w+\??\.id\b/,
|
|
806
|
+
// `user.id ?` / `&&` / `||` / `===` / `!==` / `==` / `!=`
|
|
807
|
+
/\w+\??\.id\s*(\?[^.]|&&|\|\||===|!==|==|!=)/,
|
|
808
|
+
// any group / role check the brief asked for
|
|
809
|
+
/\bgroupIds\b/,
|
|
810
|
+
/\broles\b/,
|
|
811
|
+
];
|
|
812
|
+
|
|
813
|
+
function _writeGatedOnUserRules(source) {
|
|
814
|
+
const code = _stripNonCode(source);
|
|
815
|
+
const call = /\buseDatastoreMutation\s*\(/.exec(code);
|
|
816
|
+
if (!call) return [];
|
|
817
|
+
if (_IDENTITY_GUARD_RES.some((re) => re.test(code))) return [];
|
|
818
|
+
const line = code.slice(0, call.index).split(/\r?\n/).length;
|
|
819
|
+
return [
|
|
820
|
+
{
|
|
821
|
+
rule: "write-not-gated-on-user",
|
|
822
|
+
severity: "warning",
|
|
823
|
+
// Kept under ~210 chars: a finding is truncated at 300 downstream, and
|
|
824
|
+
// the fix instruction is the half worth keeping.
|
|
825
|
+
label:
|
|
826
|
+
`writes with useDatastoreMutation() but never checks who is signed ` +
|
|
827
|
+
`in - read useUser(), and when !user.id render the action inactive ` +
|
|
828
|
+
`with a "sign in" line instead of a live button that can only fail.`,
|
|
829
|
+
line,
|
|
830
|
+
snippet: (source.split(/\r?\n/)[line - 1] || "").trim().slice(0, 200),
|
|
831
|
+
},
|
|
832
|
+
];
|
|
833
|
+
}
|
|
834
|
+
|
|
792
835
|
function _hardcodedCurrencyLabelRules(source) {
|
|
793
836
|
const code = _stripNonCode(source, { keepStrings: true });
|
|
794
837
|
if (!code.includes(REQUEST_PAYMENT_CALL)) return [];
|
|
@@ -1039,6 +1082,7 @@ function lintSource(source, options) {
|
|
|
1039
1082
|
// sc-4913 — soft warning: a measured width that includes the widget's own
|
|
1040
1083
|
// padding wraps the last grid column into an empty one.
|
|
1041
1084
|
findings.push(..._measuredPaddingRules(source));
|
|
1085
|
+
findings.push(..._writeGatedOnUserRules(source));
|
|
1042
1086
|
// sc-4650 — soft warning: every payment refusal reported as "try again".
|
|
1043
1087
|
findings.push(..._paymentCurrencyRules(source));
|
|
1044
1088
|
findings.push(..._hardcodedCurrencyLabelRules(source));
|
package/dist/linter.js
CHANGED
|
@@ -921,6 +921,49 @@ const CURRENCY_LABEL_RES = [
|
|
|
921
921
|
/(?:\d|\})\s*\b(?:SEK|NOK|DKK|EUR|GBP|USD|CHF|PLN|CZK|HUF|JPY|INR)\b/,
|
|
922
922
|
];
|
|
923
923
|
|
|
924
|
+
// sc-4985 — soft warning: a widget that writes must decide what a signed-OUT
|
|
925
|
+
// visitor sees. A write needs a signed-in app user, so an anonymous visitor
|
|
926
|
+
// handed a live "Save" / "Book" / "Delete" button can only tap it and fail —
|
|
927
|
+
// the failure the gate exists to spare them. Satisfied by any identity guard:
|
|
928
|
+
// a negated or compared `.id`, or a `groupIds` / `roles` check. Reading
|
|
929
|
+
// `useUser().id` purely as a VALUE (the USER-column write pattern) is NOT a
|
|
930
|
+
// guard, which is why an operator has to follow it.
|
|
931
|
+
//
|
|
932
|
+
// Conservative on purpose: an unrelated `.id` comparison elsewhere in the
|
|
933
|
+
// source silences the rule. A warning that occasionally stays quiet is far
|
|
934
|
+
// cheaper than one that cries wolf on correct code.
|
|
935
|
+
const _IDENTITY_GUARD_RES = [
|
|
936
|
+
// `!user.id` / `!user?.id`
|
|
937
|
+
/![\s(]*\w+\??\.id\b/,
|
|
938
|
+
// `user.id ?` / `&&` / `||` / `===` / `!==` / `==` / `!=`
|
|
939
|
+
/\w+\??\.id\s*(\?[^.]|&&|\|\||===|!==|==|!=)/,
|
|
940
|
+
// any group / role check the brief asked for
|
|
941
|
+
/\bgroupIds\b/,
|
|
942
|
+
/\broles\b/,
|
|
943
|
+
];
|
|
944
|
+
|
|
945
|
+
function _writeGatedOnUserRules(source) {
|
|
946
|
+
const code = _stripNonCode(source);
|
|
947
|
+
const call = /\buseDatastoreMutation\s*\(/.exec(code);
|
|
948
|
+
if (!call) return [];
|
|
949
|
+
if (_IDENTITY_GUARD_RES.some((re) => re.test(code))) return [];
|
|
950
|
+
const line = code.slice(0, call.index).split(/\r?\n/).length;
|
|
951
|
+
return [
|
|
952
|
+
{
|
|
953
|
+
rule: "write-not-gated-on-user",
|
|
954
|
+
severity: "warning",
|
|
955
|
+
// Kept under ~210 chars: a finding is truncated at 300 downstream, and
|
|
956
|
+
// the fix instruction is the half worth keeping.
|
|
957
|
+
label:
|
|
958
|
+
`writes with useDatastoreMutation() but never checks who is signed ` +
|
|
959
|
+
`in - read useUser(), and when !user.id render the action inactive ` +
|
|
960
|
+
`with a "sign in" line instead of a live button that can only fail.`,
|
|
961
|
+
line,
|
|
962
|
+
snippet: (source.split(/\r?\n/)[line - 1] || "").trim().slice(0, 200),
|
|
963
|
+
},
|
|
964
|
+
];
|
|
965
|
+
}
|
|
966
|
+
|
|
924
967
|
function _hardcodedCurrencyLabelRules(source) {
|
|
925
968
|
const code = _stripNonCode(source, { keepStrings: true });
|
|
926
969
|
if (!code.includes(REQUEST_PAYMENT_CALL)) return [];
|
|
@@ -1201,6 +1244,7 @@ export function lintSource(source, options) {
|
|
|
1201
1244
|
// sc-4913 — soft warning: a measured width that includes the widget's own
|
|
1202
1245
|
// padding wraps the last grid column into an empty one.
|
|
1203
1246
|
findings.push(..._measuredPaddingRules(source));
|
|
1247
|
+
findings.push(..._writeGatedOnUserRules(source));
|
|
1204
1248
|
// sc-4650 — soft warning: every payment refusal reported as "try again".
|
|
1205
1249
|
findings.push(..._paymentCurrencyRules(source));
|
|
1206
1250
|
findings.push(..._hardcodedCurrencyLabelRules(source));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@colixsystems/widget-sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.89.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-payments.test.js src/__tests__/hooks-record-permissions.test.js src/__tests__/hooks-geolocation.test.js src/__tests__/hooks-section-empty.test.js src/__tests__/hooks-widget-event.test.js src/__tests__/hooks-widget-input.test.js src/__tests__/hooks-identification.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-measured-padding.test.js src/__tests__/linter-payment-error.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 src/__tests__/toast-host.test.js src/__tests__/hooks-domain-error-mapping.test.js src/__tests__/linter-datastore-error.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-payments.test.js src/__tests__/hooks-record-permissions.test.js src/__tests__/hooks-geolocation.test.js src/__tests__/hooks-section-empty.test.js src/__tests__/hooks-widget-event.test.js src/__tests__/hooks-widget-input.test.js src/__tests__/hooks-identification.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-measured-padding.test.js src/__tests__/linter-payment-error.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 src/__tests__/toast-host.test.js src/__tests__/hooks-domain-error-mapping.test.js src/__tests__/linter-datastore-error.test.js src/__tests__/linter-write-gating.test.js"
|
|
52
52
|
},
|
|
53
53
|
"engines": {
|
|
54
54
|
"node": ">=18"
|