@colixsystems/widget-sdk 0.36.0 → 0.37.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 +5 -1
- package/dist/contract.cjs +28 -1
- package/dist/contract.js +28 -1
- package/dist/linter.cjs +152 -3
- package/dist/linter.js +181 -18
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -47,7 +47,11 @@ See the design reference for the full architecture: [`docs/architecture/widget-m
|
|
|
47
47
|
|
|
48
48
|
## Status
|
|
49
49
|
|
|
50
|
-
`v0.
|
|
50
|
+
`v0.37.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**.
|
|
51
|
+
|
|
52
|
+
### What's new in 0.37.0
|
|
53
|
+
|
|
54
|
+
**`react/jsx-runtime` + `react/jsx-dev-runtime` are now vetted imports.** A widget bundle compiled with React's *automatic* JSX runtime (Vite/esbuild's default) emits `import { jsx, jsxs, Fragment } from "react/jsx-runtime"` — code the author never writes by hand. The runtime already treated these as host-provided (the web loader shims both, the AI-agent sandbox stubs them, and the Developer guide documents them as externalized), but the linter's vetted list did not list them, so such a bundle failed publish static analysis with `import-not-vetted` on `react/jsx-runtime`. Both are now on `CONTRACT.vettedImports` as `core` subpaths of the already-vetted `react`. **`CONTRACT.version` → `1.27.0`** (additive: two vetted core subpaths). No existing entry changed shape.
|
|
51
55
|
|
|
52
56
|
### What's new in 0.36.0
|
|
53
57
|
|
package/dist/contract.cjs
CHANGED
|
@@ -1057,6 +1057,20 @@ const VETTED_IMPORTS = [
|
|
|
1057
1057
|
category: "core",
|
|
1058
1058
|
description: "React. Hooks, JSX, lifecycle. Unchanged.",
|
|
1059
1059
|
},
|
|
1060
|
+
{
|
|
1061
|
+
specifier: "react/jsx-runtime",
|
|
1062
|
+
platforms: ["web", "native"],
|
|
1063
|
+
category: "core",
|
|
1064
|
+
description:
|
|
1065
|
+
"React's automatic JSX runtime (jsx/jsxs/Fragment). Not hand-written — the JSX transform injects this import when a bundle is compiled with the automatic runtime. Host-provided on both platforms (the web loader shims it; Metro resolves the real subpath of react), so it is externalized exactly like react.",
|
|
1066
|
+
},
|
|
1067
|
+
{
|
|
1068
|
+
specifier: "react/jsx-dev-runtime",
|
|
1069
|
+
platforms: ["web", "native"],
|
|
1070
|
+
category: "core",
|
|
1071
|
+
description:
|
|
1072
|
+
"React's automatic JSX dev runtime (jsxDEV/Fragment). The development-mode counterpart to react/jsx-runtime, injected by the JSX transform in dev builds. Host-provided on both platforms, externalized like react.",
|
|
1073
|
+
},
|
|
1060
1074
|
{
|
|
1061
1075
|
specifier: "@colixsystems/widget-sdk",
|
|
1062
1076
|
platforms: ["web", "native"],
|
|
@@ -1459,7 +1473,20 @@ const CONTRACT = deepFreeze({
|
|
|
1459
1473
|
// `users.write:*`. New linter rule `scope-required-for-user-delete`
|
|
1460
1474
|
// flags a widget that calls `.remove()` without declaring the scope.
|
|
1461
1475
|
// No hook signature changed; `useUsers().remove` is unchanged.
|
|
1462
|
-
|
|
1476
|
+
//
|
|
1477
|
+
// 1.27.0: additive — the vetted import allowlist gains `react/jsx-runtime`
|
|
1478
|
+
// and `react/jsx-dev-runtime`. These are subpaths of the already-vetted
|
|
1479
|
+
// `react`, injected automatically by the JSX transform when a bundle is
|
|
1480
|
+
// compiled with the automatic JSX runtime (Vite/esbuild's default). The
|
|
1481
|
+
// runtime already treats them as host-provided (the web loader shims both
|
|
1482
|
+
// in widgetLoader.js, the AI-agent sandbox stubs them, and the Developer
|
|
1483
|
+
// guide documents them as externalized), but the linter's vetted list did
|
|
1484
|
+
// not list them — so a first-party/marketplace bundle built with the
|
|
1485
|
+
// automatic runtime failed publish static analysis with `import-not-vetted`
|
|
1486
|
+
// on `react/jsx-runtime`. Adding them converges the linter onto the same
|
|
1487
|
+
// contract every other surface already honoured (CLAUDE.md §3). No
|
|
1488
|
+
// existing entry changed shape — minor bump on the pre-1.0 channel.
|
|
1489
|
+
version: "1.27.0",
|
|
1463
1490
|
hooks: HOOKS,
|
|
1464
1491
|
primitives: PRIMITIVES,
|
|
1465
1492
|
manifestSchema: MANIFEST_SCHEMA,
|
package/dist/contract.js
CHANGED
|
@@ -1057,6 +1057,20 @@ const VETTED_IMPORTS = [
|
|
|
1057
1057
|
category: "core",
|
|
1058
1058
|
description: "React. Hooks, JSX, lifecycle. Unchanged.",
|
|
1059
1059
|
},
|
|
1060
|
+
{
|
|
1061
|
+
specifier: "react/jsx-runtime",
|
|
1062
|
+
platforms: ["web", "native"],
|
|
1063
|
+
category: "core",
|
|
1064
|
+
description:
|
|
1065
|
+
"React's automatic JSX runtime (jsx/jsxs/Fragment). Not hand-written — the JSX transform injects this import when a bundle is compiled with the automatic runtime. Host-provided on both platforms (the web loader shims it; Metro resolves the real subpath of react), so it is externalized exactly like react.",
|
|
1066
|
+
},
|
|
1067
|
+
{
|
|
1068
|
+
specifier: "react/jsx-dev-runtime",
|
|
1069
|
+
platforms: ["web", "native"],
|
|
1070
|
+
category: "core",
|
|
1071
|
+
description:
|
|
1072
|
+
"React's automatic JSX dev runtime (jsxDEV/Fragment). The development-mode counterpart to react/jsx-runtime, injected by the JSX transform in dev builds. Host-provided on both platforms, externalized like react.",
|
|
1073
|
+
},
|
|
1060
1074
|
{
|
|
1061
1075
|
specifier: "@colixsystems/widget-sdk",
|
|
1062
1076
|
platforms: ["web", "native"],
|
|
@@ -1459,7 +1473,20 @@ const CONTRACT = deepFreeze({
|
|
|
1459
1473
|
// `users.write:*`. New linter rule `scope-required-for-user-delete`
|
|
1460
1474
|
// flags a widget that calls `.remove()` without declaring the scope.
|
|
1461
1475
|
// No hook signature changed; `useUsers().remove` is unchanged.
|
|
1462
|
-
|
|
1476
|
+
//
|
|
1477
|
+
// 1.27.0: additive — the vetted import allowlist gains `react/jsx-runtime`
|
|
1478
|
+
// and `react/jsx-dev-runtime`. These are subpaths of the already-vetted
|
|
1479
|
+
// `react`, injected automatically by the JSX transform when a bundle is
|
|
1480
|
+
// compiled with the automatic JSX runtime (Vite/esbuild's default). The
|
|
1481
|
+
// runtime already treats them as host-provided (the web loader shims both
|
|
1482
|
+
// in widgetLoader.js, the AI-agent sandbox stubs them, and the Developer
|
|
1483
|
+
// guide documents them as externalized), but the linter's vetted list did
|
|
1484
|
+
// not list them — so a first-party/marketplace bundle built with the
|
|
1485
|
+
// automatic runtime failed publish static analysis with `import-not-vetted`
|
|
1486
|
+
// on `react/jsx-runtime`. Adding them converges the linter onto the same
|
|
1487
|
+
// contract every other surface already honoured (CLAUDE.md §3). No
|
|
1488
|
+
// existing entry changed shape — minor bump on the pre-1.0 channel.
|
|
1489
|
+
version: "1.27.0",
|
|
1463
1490
|
hooks: HOOKS,
|
|
1464
1491
|
primitives: PRIMITIVES,
|
|
1465
1492
|
manifestSchema: MANIFEST_SCHEMA,
|
package/dist/linter.cjs
CHANGED
|
@@ -54,6 +54,149 @@ const CONTRACT_RULES = CONTRACT.bannedApis.map((b) =>
|
|
|
54
54
|
_ruleForIdentifier(b.identifier, b.reason),
|
|
55
55
|
);
|
|
56
56
|
|
|
57
|
+
// Replace the *content* of comments and string / template literals with
|
|
58
|
+
// spaces so the banned-identifier scan only ever sees executable code. A
|
|
59
|
+
// banned host-escape identifier (`window`, `document`, `eval`, `process`, …)
|
|
60
|
+
// is only dangerous as a real identifier reference — never as prose in a
|
|
61
|
+
// `//` comment or as character data inside a string — so matching the bare
|
|
62
|
+
// word there is a false positive that blocks an otherwise-clean widget (a
|
|
63
|
+
// comment that reads "the hour window the grid renders" must not trip
|
|
64
|
+
// `no-window`).
|
|
65
|
+
//
|
|
66
|
+
// Newlines are preserved verbatim so reported line numbers still line up
|
|
67
|
+
// with the original source. Template-literal `${ … }` expression holes are
|
|
68
|
+
// left intact: real code lives there and must still be scanned (`${window}`
|
|
69
|
+
// is a genuine escape). Backslash escapes inside strings/templates are
|
|
70
|
+
// consumed so an escaped quote (`"\""`) doesn't end the literal early.
|
|
71
|
+
function _stripNonCode(source) {
|
|
72
|
+
let out = "";
|
|
73
|
+
const n = source.length;
|
|
74
|
+
let mode = "code"; // code | line | block | sq | dq | tmpl
|
|
75
|
+
// Brace depth, plus a stack of the depths at which an enclosing template
|
|
76
|
+
// literal resumes — lets a `${ … }` hole (which may itself contain `{}`,
|
|
77
|
+
// strings, or nested templates) be told apart from the literal text.
|
|
78
|
+
let braceDepth = 0;
|
|
79
|
+
const tmplStack = [];
|
|
80
|
+
const keep = (ch) => {
|
|
81
|
+
out += ch;
|
|
82
|
+
};
|
|
83
|
+
const blank = (ch) => {
|
|
84
|
+
out += ch === "\n" || ch === "\r" ? ch : " ";
|
|
85
|
+
};
|
|
86
|
+
let i = 0;
|
|
87
|
+
while (i < n) {
|
|
88
|
+
const ch = source[i];
|
|
89
|
+
const nx = source[i + 1];
|
|
90
|
+
if (mode === "code") {
|
|
91
|
+
if (ch === "/" && nx === "/") {
|
|
92
|
+
mode = "line";
|
|
93
|
+
blank(ch);
|
|
94
|
+
blank(nx);
|
|
95
|
+
i += 2;
|
|
96
|
+
} else if (ch === "/" && nx === "*") {
|
|
97
|
+
mode = "block";
|
|
98
|
+
blank(ch);
|
|
99
|
+
blank(nx);
|
|
100
|
+
i += 2;
|
|
101
|
+
} else if (ch === "'") {
|
|
102
|
+
mode = "sq";
|
|
103
|
+
blank(ch);
|
|
104
|
+
i += 1;
|
|
105
|
+
} else if (ch === '"') {
|
|
106
|
+
mode = "dq";
|
|
107
|
+
blank(ch);
|
|
108
|
+
i += 1;
|
|
109
|
+
} else if (ch === "`") {
|
|
110
|
+
mode = "tmpl";
|
|
111
|
+
blank(ch);
|
|
112
|
+
i += 1;
|
|
113
|
+
} else if (ch === "{") {
|
|
114
|
+
braceDepth += 1;
|
|
115
|
+
keep(ch);
|
|
116
|
+
i += 1;
|
|
117
|
+
} else if (ch === "}") {
|
|
118
|
+
braceDepth -= 1;
|
|
119
|
+
if (
|
|
120
|
+
tmplStack.length > 0 &&
|
|
121
|
+
tmplStack[tmplStack.length - 1] === braceDepth
|
|
122
|
+
) {
|
|
123
|
+
tmplStack.pop();
|
|
124
|
+
mode = "tmpl";
|
|
125
|
+
blank(ch);
|
|
126
|
+
} else {
|
|
127
|
+
keep(ch);
|
|
128
|
+
}
|
|
129
|
+
i += 1;
|
|
130
|
+
} else {
|
|
131
|
+
keep(ch);
|
|
132
|
+
i += 1;
|
|
133
|
+
}
|
|
134
|
+
} else if (mode === "line") {
|
|
135
|
+
if (ch === "\n") {
|
|
136
|
+
mode = "code";
|
|
137
|
+
keep(ch);
|
|
138
|
+
} else {
|
|
139
|
+
blank(ch);
|
|
140
|
+
}
|
|
141
|
+
i += 1;
|
|
142
|
+
} else if (mode === "block") {
|
|
143
|
+
if (ch === "*" && nx === "/") {
|
|
144
|
+
mode = "code";
|
|
145
|
+
blank(ch);
|
|
146
|
+
blank(nx);
|
|
147
|
+
i += 2;
|
|
148
|
+
} else {
|
|
149
|
+
blank(ch);
|
|
150
|
+
i += 1;
|
|
151
|
+
}
|
|
152
|
+
} else if (mode === "sq" || mode === "dq") {
|
|
153
|
+
const quote = mode === "sq" ? "'" : '"';
|
|
154
|
+
if (ch === "\\") {
|
|
155
|
+
blank(ch);
|
|
156
|
+
if (i + 1 < n) blank(nx);
|
|
157
|
+
i += 2;
|
|
158
|
+
} else if (ch === quote) {
|
|
159
|
+
mode = "code";
|
|
160
|
+
blank(ch);
|
|
161
|
+
i += 1;
|
|
162
|
+
} else if (ch === "\n") {
|
|
163
|
+
// A bare newline terminates an unterminated string in JS; bail back
|
|
164
|
+
// to code so malformed input can't blank the rest of the file.
|
|
165
|
+
mode = "code";
|
|
166
|
+
keep(ch);
|
|
167
|
+
i += 1;
|
|
168
|
+
} else {
|
|
169
|
+
blank(ch);
|
|
170
|
+
i += 1;
|
|
171
|
+
}
|
|
172
|
+
} else {
|
|
173
|
+
// mode === "tmpl"
|
|
174
|
+
if (ch === "\\") {
|
|
175
|
+
blank(ch);
|
|
176
|
+
if (i + 1 < n) blank(nx);
|
|
177
|
+
i += 2;
|
|
178
|
+
} else if (ch === "`") {
|
|
179
|
+
mode = "code";
|
|
180
|
+
blank(ch);
|
|
181
|
+
i += 1;
|
|
182
|
+
} else if (ch === "$" && nx === "{") {
|
|
183
|
+
// Enter an expression hole. Remember the brace depth the template
|
|
184
|
+
// resumes at, then count the `{` so its matching `}` is recognised.
|
|
185
|
+
tmplStack.push(braceDepth);
|
|
186
|
+
braceDepth += 1;
|
|
187
|
+
mode = "code";
|
|
188
|
+
keep(ch);
|
|
189
|
+
keep(nx);
|
|
190
|
+
i += 2;
|
|
191
|
+
} else {
|
|
192
|
+
blank(ch);
|
|
193
|
+
i += 1;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
return out;
|
|
198
|
+
}
|
|
199
|
+
|
|
57
200
|
// REQ-WSDK-PLATFORM: `no-axios-import` is GONE. axios is on the vetted
|
|
58
201
|
// import list now (`CONTRACT.vettedImports`). See linter.js for the
|
|
59
202
|
// source-of-truth comment.
|
|
@@ -387,16 +530,22 @@ function lintSource(source, options) {
|
|
|
387
530
|
}
|
|
388
531
|
const findings = [];
|
|
389
532
|
const lines = source.split(/\r?\n/);
|
|
533
|
+
// Scan code with comments + string/template text blanked out so a banned
|
|
534
|
+
// identifier only fires on an actual code reference, not on the same word
|
|
535
|
+
// appearing in prose or string data. `codeLines` lines up 1:1 with `lines`
|
|
536
|
+
// (masking preserves newlines), so the reported snippet still comes from
|
|
537
|
+
// the original source.
|
|
538
|
+
const codeLines = _stripNonCode(source).split(/\r?\n/);
|
|
390
539
|
for (let i = 0; i < lines.length; i++) {
|
|
391
|
-
const
|
|
540
|
+
const codeLine = codeLines[i];
|
|
392
541
|
for (const rule of RULES) {
|
|
393
|
-
if (rule.pattern.test(
|
|
542
|
+
if (rule.pattern.test(codeLine)) {
|
|
394
543
|
findings.push({
|
|
395
544
|
rule: rule.id,
|
|
396
545
|
severity: "error",
|
|
397
546
|
label: rule.label,
|
|
398
547
|
line: i + 1,
|
|
399
|
-
snippet:
|
|
548
|
+
snippet: lines[i].trim().slice(0, 200),
|
|
400
549
|
});
|
|
401
550
|
}
|
|
402
551
|
}
|
package/dist/linter.js
CHANGED
|
@@ -55,6 +55,149 @@ const CONTRACT_RULES = CONTRACT.bannedApis.map((b) =>
|
|
|
55
55
|
_ruleForIdentifier(b.identifier, b.reason),
|
|
56
56
|
);
|
|
57
57
|
|
|
58
|
+
// Replace the *content* of comments and string / template literals with
|
|
59
|
+
// spaces so the banned-identifier scan only ever sees executable code. A
|
|
60
|
+
// banned host-escape identifier (`window`, `document`, `eval`, `process`, …)
|
|
61
|
+
// is only dangerous as a real identifier reference — never as prose in a
|
|
62
|
+
// `//` comment or as character data inside a string — so matching the bare
|
|
63
|
+
// word there is a false positive that blocks an otherwise-clean widget (a
|
|
64
|
+
// comment that reads "the hour window the grid renders" must not trip
|
|
65
|
+
// `no-window`).
|
|
66
|
+
//
|
|
67
|
+
// Newlines are preserved verbatim so reported line numbers still line up
|
|
68
|
+
// with the original source. Template-literal `${ … }` expression holes are
|
|
69
|
+
// left intact: real code lives there and must still be scanned (`${window}`
|
|
70
|
+
// is a genuine escape). Backslash escapes inside strings/templates are
|
|
71
|
+
// consumed so an escaped quote (`"\""`) doesn't end the literal early.
|
|
72
|
+
function _stripNonCode(source) {
|
|
73
|
+
let out = "";
|
|
74
|
+
const n = source.length;
|
|
75
|
+
let mode = "code"; // code | line | block | sq | dq | tmpl
|
|
76
|
+
// Brace depth, plus a stack of the depths at which an enclosing template
|
|
77
|
+
// literal resumes — lets a `${ … }` hole (which may itself contain `{}`,
|
|
78
|
+
// strings, or nested templates) be told apart from the literal text.
|
|
79
|
+
let braceDepth = 0;
|
|
80
|
+
const tmplStack = [];
|
|
81
|
+
const keep = (ch) => {
|
|
82
|
+
out += ch;
|
|
83
|
+
};
|
|
84
|
+
const blank = (ch) => {
|
|
85
|
+
out += ch === "\n" || ch === "\r" ? ch : " ";
|
|
86
|
+
};
|
|
87
|
+
let i = 0;
|
|
88
|
+
while (i < n) {
|
|
89
|
+
const ch = source[i];
|
|
90
|
+
const nx = source[i + 1];
|
|
91
|
+
if (mode === "code") {
|
|
92
|
+
if (ch === "/" && nx === "/") {
|
|
93
|
+
mode = "line";
|
|
94
|
+
blank(ch);
|
|
95
|
+
blank(nx);
|
|
96
|
+
i += 2;
|
|
97
|
+
} else if (ch === "/" && nx === "*") {
|
|
98
|
+
mode = "block";
|
|
99
|
+
blank(ch);
|
|
100
|
+
blank(nx);
|
|
101
|
+
i += 2;
|
|
102
|
+
} else if (ch === "'") {
|
|
103
|
+
mode = "sq";
|
|
104
|
+
blank(ch);
|
|
105
|
+
i += 1;
|
|
106
|
+
} else if (ch === '"') {
|
|
107
|
+
mode = "dq";
|
|
108
|
+
blank(ch);
|
|
109
|
+
i += 1;
|
|
110
|
+
} else if (ch === "`") {
|
|
111
|
+
mode = "tmpl";
|
|
112
|
+
blank(ch);
|
|
113
|
+
i += 1;
|
|
114
|
+
} else if (ch === "{") {
|
|
115
|
+
braceDepth += 1;
|
|
116
|
+
keep(ch);
|
|
117
|
+
i += 1;
|
|
118
|
+
} else if (ch === "}") {
|
|
119
|
+
braceDepth -= 1;
|
|
120
|
+
if (
|
|
121
|
+
tmplStack.length > 0 &&
|
|
122
|
+
tmplStack[tmplStack.length - 1] === braceDepth
|
|
123
|
+
) {
|
|
124
|
+
tmplStack.pop();
|
|
125
|
+
mode = "tmpl";
|
|
126
|
+
blank(ch);
|
|
127
|
+
} else {
|
|
128
|
+
keep(ch);
|
|
129
|
+
}
|
|
130
|
+
i += 1;
|
|
131
|
+
} else {
|
|
132
|
+
keep(ch);
|
|
133
|
+
i += 1;
|
|
134
|
+
}
|
|
135
|
+
} else if (mode === "line") {
|
|
136
|
+
if (ch === "\n") {
|
|
137
|
+
mode = "code";
|
|
138
|
+
keep(ch);
|
|
139
|
+
} else {
|
|
140
|
+
blank(ch);
|
|
141
|
+
}
|
|
142
|
+
i += 1;
|
|
143
|
+
} else if (mode === "block") {
|
|
144
|
+
if (ch === "*" && nx === "/") {
|
|
145
|
+
mode = "code";
|
|
146
|
+
blank(ch);
|
|
147
|
+
blank(nx);
|
|
148
|
+
i += 2;
|
|
149
|
+
} else {
|
|
150
|
+
blank(ch);
|
|
151
|
+
i += 1;
|
|
152
|
+
}
|
|
153
|
+
} else if (mode === "sq" || mode === "dq") {
|
|
154
|
+
const quote = mode === "sq" ? "'" : '"';
|
|
155
|
+
if (ch === "\\") {
|
|
156
|
+
blank(ch);
|
|
157
|
+
if (i + 1 < n) blank(nx);
|
|
158
|
+
i += 2;
|
|
159
|
+
} else if (ch === quote) {
|
|
160
|
+
mode = "code";
|
|
161
|
+
blank(ch);
|
|
162
|
+
i += 1;
|
|
163
|
+
} else if (ch === "\n") {
|
|
164
|
+
// A bare newline terminates an unterminated string in JS; bail back
|
|
165
|
+
// to code so malformed input can't blank the rest of the file.
|
|
166
|
+
mode = "code";
|
|
167
|
+
keep(ch);
|
|
168
|
+
i += 1;
|
|
169
|
+
} else {
|
|
170
|
+
blank(ch);
|
|
171
|
+
i += 1;
|
|
172
|
+
}
|
|
173
|
+
} else {
|
|
174
|
+
// mode === "tmpl"
|
|
175
|
+
if (ch === "\\") {
|
|
176
|
+
blank(ch);
|
|
177
|
+
if (i + 1 < n) blank(nx);
|
|
178
|
+
i += 2;
|
|
179
|
+
} else if (ch === "`") {
|
|
180
|
+
mode = "code";
|
|
181
|
+
blank(ch);
|
|
182
|
+
i += 1;
|
|
183
|
+
} else if (ch === "$" && nx === "{") {
|
|
184
|
+
// Enter an expression hole. Remember the brace depth the template
|
|
185
|
+
// resumes at, then count the `{` so its matching `}` is recognised.
|
|
186
|
+
tmplStack.push(braceDepth);
|
|
187
|
+
braceDepth += 1;
|
|
188
|
+
mode = "code";
|
|
189
|
+
keep(ch);
|
|
190
|
+
keep(nx);
|
|
191
|
+
i += 2;
|
|
192
|
+
} else {
|
|
193
|
+
blank(ch);
|
|
194
|
+
i += 1;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
return out;
|
|
199
|
+
}
|
|
200
|
+
|
|
58
201
|
// Extra rules that don't map 1:1 to a banned identifier in the contract:
|
|
59
202
|
// host-internal imports that widgets must never touch.
|
|
60
203
|
//
|
|
@@ -124,9 +267,7 @@ function _classifySpecifier(spec) {
|
|
|
124
267
|
|
|
125
268
|
function _importRules(source, manifest) {
|
|
126
269
|
const findings = [];
|
|
127
|
-
const allowed = new Map(
|
|
128
|
-
CONTRACT.vettedImports.map((v) => [v.specifier, v]),
|
|
129
|
-
);
|
|
270
|
+
const allowed = new Map(CONTRACT.vettedImports.map((v) => [v.specifier, v]));
|
|
130
271
|
// Track declared `supportedPlatforms` so a widget that claims "web only"
|
|
131
272
|
// doesn't import a native-only package (and vice versa) without the
|
|
132
273
|
// marketplace listing being honest about which platforms ship.
|
|
@@ -258,7 +399,12 @@ const USER_MUTATION_METHODS = ["invite", "deactivate", "reactivate"];
|
|
|
258
399
|
// useUsers().remove() must declare `users.delete:*` so the static contract
|
|
259
400
|
// matches the backend gate on DELETE /api/v1/app/users/:userId.
|
|
260
401
|
const USER_DELETE_METHODS = ["remove"];
|
|
261
|
-
const GROUP_MUTATION_METHODS = [
|
|
402
|
+
const GROUP_MUTATION_METHODS = [
|
|
403
|
+
"create",
|
|
404
|
+
"remove",
|
|
405
|
+
"addMember",
|
|
406
|
+
"removeMember",
|
|
407
|
+
];
|
|
262
408
|
const SKIP_COMMENT = /\/\/[^\n]*@appstudio-skip-scope-check/;
|
|
263
409
|
|
|
264
410
|
function _scopeRules(source, manifest) {
|
|
@@ -277,8 +423,7 @@ function _scopeRules(source, manifest) {
|
|
|
277
423
|
if (!reads) {
|
|
278
424
|
findings.push({
|
|
279
425
|
rule: "scope-required-for-useUsers",
|
|
280
|
-
label:
|
|
281
|
-
"useUsers() requires `users.read:*` in manifest.requestedScopes",
|
|
426
|
+
label: "useUsers() requires `users.read:*` in manifest.requestedScopes",
|
|
282
427
|
line: 0,
|
|
283
428
|
snippet: "",
|
|
284
429
|
});
|
|
@@ -308,10 +453,7 @@ function _scopeRules(source, manifest) {
|
|
|
308
453
|
for (const m of USER_MUTATION_METHODS) {
|
|
309
454
|
const re = new RegExp(`\\.${m}\\s*\\(`);
|
|
310
455
|
if (re.test(line)) {
|
|
311
|
-
if (
|
|
312
|
-
!declared.has("users.write:*") &&
|
|
313
|
-
!declared.has("users.write")
|
|
314
|
-
) {
|
|
456
|
+
if (!declared.has("users.write:*") && !declared.has("users.write")) {
|
|
315
457
|
findings.push({
|
|
316
458
|
rule: "scope-required-for-user-mutation",
|
|
317
459
|
label: `useUsers().${m}() requires \`users.write:*\` in manifest.requestedScopes`,
|
|
@@ -400,7 +542,13 @@ function _manifestActionRules(manifest) {
|
|
|
400
542
|
const validTriggers = new Set(CONTRACT.actionTriggerTypes);
|
|
401
543
|
const maxBytes = CONTRACT.actionScriptMaxBytes;
|
|
402
544
|
const push = (label) =>
|
|
403
|
-
findings.push({
|
|
545
|
+
findings.push({
|
|
546
|
+
rule: "manifest-action",
|
|
547
|
+
severity: "error",
|
|
548
|
+
label,
|
|
549
|
+
line: 0,
|
|
550
|
+
snippet: "",
|
|
551
|
+
});
|
|
404
552
|
if (!Array.isArray(manifest.actions)) {
|
|
405
553
|
push("manifest.actions must be an array (omit it or use [] for none)");
|
|
406
554
|
return findings;
|
|
@@ -422,9 +570,16 @@ function _manifestActionRules(manifest) {
|
|
|
422
570
|
push("manifest.actions[].name must be a non-empty string");
|
|
423
571
|
}
|
|
424
572
|
if (!validTriggers.has(a.triggerType)) {
|
|
425
|
-
push(
|
|
426
|
-
|
|
427
|
-
|
|
573
|
+
push(
|
|
574
|
+
`manifest.actions[].triggerType must be one of ${[...validTriggers].join(", ")}`,
|
|
575
|
+
);
|
|
576
|
+
} else if (
|
|
577
|
+
a.triggerType === "schedule" &&
|
|
578
|
+
(typeof a.scheduleCron !== "string" || !a.scheduleCron)
|
|
579
|
+
) {
|
|
580
|
+
push(
|
|
581
|
+
"manifest.actions[].scheduleCron is required when triggerType is 'schedule'",
|
|
582
|
+
);
|
|
428
583
|
}
|
|
429
584
|
if (typeof a.scriptSource !== "string" || a.scriptSource.length === 0) {
|
|
430
585
|
push("manifest.actions[].scriptSource must be a non-empty string");
|
|
@@ -438,7 +593,9 @@ function _manifestActionRules(manifest) {
|
|
|
438
593
|
}
|
|
439
594
|
}
|
|
440
595
|
if (a.triggerTableId !== undefined || a.apiKeyId !== undefined) {
|
|
441
|
-
push(
|
|
596
|
+
push(
|
|
597
|
+
"manifest.actions[] must not include triggerTableId or apiKeyId — those are tenant-local and bound after install",
|
|
598
|
+
);
|
|
442
599
|
}
|
|
443
600
|
}
|
|
444
601
|
return findings;
|
|
@@ -461,16 +618,22 @@ export function lintSource(source, options) {
|
|
|
461
618
|
}
|
|
462
619
|
const findings = [];
|
|
463
620
|
const lines = source.split(/\r?\n/);
|
|
621
|
+
// Scan code with comments + string/template text blanked out so a banned
|
|
622
|
+
// identifier only fires on an actual code reference, not on the same word
|
|
623
|
+
// appearing in prose or string data. `codeLines` lines up 1:1 with `lines`
|
|
624
|
+
// (masking preserves newlines), so the reported snippet still comes from
|
|
625
|
+
// the original source.
|
|
626
|
+
const codeLines = _stripNonCode(source).split(/\r?\n/);
|
|
464
627
|
for (let i = 0; i < lines.length; i++) {
|
|
465
|
-
const
|
|
628
|
+
const codeLine = codeLines[i];
|
|
466
629
|
for (const rule of RULES) {
|
|
467
|
-
if (rule.pattern.test(
|
|
630
|
+
if (rule.pattern.test(codeLine)) {
|
|
468
631
|
findings.push({
|
|
469
632
|
rule: rule.id,
|
|
470
633
|
severity: "error",
|
|
471
634
|
label: rule.label,
|
|
472
635
|
line: i + 1,
|
|
473
|
-
snippet:
|
|
636
|
+
snippet: lines[i].trim().slice(0, 200),
|
|
474
637
|
});
|
|
475
638
|
}
|
|
476
639
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@colixsystems/widget-sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.37.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",
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
],
|
|
36
36
|
"scripts": {
|
|
37
37
|
"build": "node scripts/build.js",
|
|
38
|
-
"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-mutation.test.js src/__tests__/hooks-record-permissions.test.js src/__tests__/hooks-subscription.test.js src/__tests__/linter-users-scope.test.js src/__tests__/manifest-actions.test.js src/__tests__/widget-translations.test.js src/__tests__/devserver.test.js"
|
|
38
|
+
"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-mutation.test.js src/__tests__/hooks-record-permissions.test.js src/__tests__/hooks-subscription.test.js src/__tests__/linter-users-scope.test.js src/__tests__/linter-comments.test.js src/__tests__/manifest-actions.test.js src/__tests__/widget-translations.test.js src/__tests__/devserver.test.js"
|
|
39
39
|
},
|
|
40
40
|
"engines": {
|
|
41
41
|
"node": ">=18"
|