@colixsystems/widget-sdk 0.288.0 → 0.290.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 +9 -1
- package/dist/contract.cjs +58 -2
- package/dist/contract.js +58 -2
- package/dist/hooks.js +109 -0
- package/dist/index.d.ts +63 -1
- package/dist/index.js +2 -0
- package/dist/index.native.js +2 -0
- package/dist/linter.cjs +10 -0
- package/dist/linter.js +10 -0
- package/dist/manifest.cjs +2 -2
- package/dist/manifest.js +2 -2
- package/dist/property-schema.js +4 -0
- package/package.json +1 -1
- package/src/contract.js +58 -2
package/README.md
CHANGED
|
@@ -90,7 +90,15 @@ See the design reference for the full architecture: [`docs/architecture/widget-m
|
|
|
90
90
|
|
|
91
91
|
## Status
|
|
92
92
|
|
|
93
|
-
`v0.
|
|
93
|
+
`v0.290.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**.
|
|
94
|
+
|
|
95
|
+
### What's new in 0.290.0 (contract 1.238.0)
|
|
96
|
+
|
|
97
|
+
**New `useConnector(connectorRef)` hook — a widget can call one of the workspace's REST connectors and use the answer itself.** Bind a connector with a `{ type: "connectorRef" }` property and declare `connectors.call:*` in `requestedScopes`. `call(operation, { params, query, body })` names an operation the workspace listed for apps on that connector — never a method or a path — and resolves `{ status, content_type, body }` with the outside API's status and parsed body. The whole call has 250 ms, token exchange included: a slower outside API is cut off and the call rejects with `UPSTREAM_TIMEOUT`. The credential is added on the server and never reaches the app. A new `connectors` host client (`createConnectorsClient` in `@colixsystems/actions-client`), the `connectorRef` property type, the `connectors.call:*` scope and a `ConnectorError`. Everything is additive.
|
|
98
|
+
|
|
99
|
+
### What's new in 0.289.0 (contract 1.237.0)
|
|
100
|
+
|
|
101
|
+
**`manifest.translations` may declare up to 500 keys (was 200).** A widget with many screens, or one that ships every label of a large form, no longer runs out of keys. Widening: every manifest that validated before still does.
|
|
94
102
|
|
|
95
103
|
### What's new in 0.288.0 (contract 1.236.0)
|
|
96
104
|
|
package/dist/contract.cjs
CHANGED
|
@@ -2299,6 +2299,46 @@ const HOOKS = [
|
|
|
2299
2299
|
requiredContextSlice: ["actions.run"],
|
|
2300
2300
|
scopes: ["actions.run:*"],
|
|
2301
2301
|
},
|
|
2302
|
+
{
|
|
2303
|
+
name: "useConnector",
|
|
2304
|
+
signature: "useConnector(connectorRef)",
|
|
2305
|
+
description:
|
|
2306
|
+
"sc-15659 — call one of the WORKSPACE's REST connectors (a configured " +
|
|
2307
|
+
"outside API) straight from the widget, and use the answer in the " +
|
|
2308
|
+
"widget. call(operation, { params, query, body }) names an OPERATION " +
|
|
2309
|
+
"the workspace listed for apps on that connector, never a method or a " +
|
|
2310
|
+
"path: the server builds the request from the listing and adds the " +
|
|
2311
|
+
"connector's credential, which never reaches the app. `params` fills " +
|
|
2312
|
+
"the operation's {name} path segments (one whole segment each, no " +
|
|
2313
|
+
"slashes); `query` may use only the names the operation lists; `body` " +
|
|
2314
|
+
"only where the operation takes one. call resolves with { status, " +
|
|
2315
|
+
"content_type, body } — the outside API's status (a non-2xx answer " +
|
|
2316
|
+
"still resolves: branch on status) and its parsed JSON or text body. " +
|
|
2317
|
+
"The whole call must finish within 250 ms, token exchange included: a " +
|
|
2318
|
+
"slower outside API is cut off and the call rejects with UPSTREAM_TIMEOUT. " +
|
|
2319
|
+
"May run on mount for a read operation; an operation that changes data " +
|
|
2320
|
+
"belongs in an event handler. `connectorRef` is a connector id an " +
|
|
2321
|
+
"author bound through a `connectorRef` property — when it is missing, " +
|
|
2322
|
+
"or the host brokers no connectors client, `available` is false and " +
|
|
2323
|
+
"call() rejects UNAVAILABLE; render a visible 'pick a connector' state, " +
|
|
2324
|
+
"NEVER a blank box. Only signed-in app users may call. ConnectorError.code: " +
|
|
2325
|
+
"OPERATION_NOT_ALLOWED (403), CONNECTOR_NOT_FOUND (404, also when the " +
|
|
2326
|
+
"connector is not callable from the app) and CONNECTOR_SETUP_INCOMPLETE " +
|
|
2327
|
+
"(409) will NOT clear on a retry, so show an 'isn't available' state; " +
|
|
2328
|
+
"RATE_LIMITED (429) will; INVALID_CONNECTOR_PARAMS (400) means an " +
|
|
2329
|
+
"argument the operation does not take; UPSTREAM_ERROR (502) and " +
|
|
2330
|
+
"UPSTREAM_TIMEOUT (504) mean the outside API failed; AUTH_REQUIRED, " +
|
|
2331
|
+
"TOKEN_EXPIRED and TOKEN_INVALID mean the app user must sign in. NEVER " +
|
|
2332
|
+
"show an app user a raw error. Reads ctx.connectors.",
|
|
2333
|
+
returnShape: {
|
|
2334
|
+
available: "boolean // false when no connector is bound or the host brokers none",
|
|
2335
|
+
call: "(operation, { params?, query?, body? }) => Promise<{ status, content_type, body }> // rejects with ConnectorError",
|
|
2336
|
+
calling: "boolean // true while any call is in flight",
|
|
2337
|
+
error: "ConnectorError | null // the last call's error; cleared by the next call or a rebind",
|
|
2338
|
+
},
|
|
2339
|
+
requiredContextSlice: ["connectors.call"],
|
|
2340
|
+
scopes: ["connectors.call:*"],
|
|
2341
|
+
},
|
|
2302
2342
|
{
|
|
2303
2343
|
name: "useDatastoreMutation",
|
|
2304
2344
|
signature: "useDatastoreMutation(tableId)",
|
|
@@ -4176,7 +4216,7 @@ const MANIFEST_SCHEMA = {
|
|
|
4176
4216
|
type: "object",
|
|
4177
4217
|
required: false,
|
|
4178
4218
|
description:
|
|
4179
|
-
"Optional (REQ-L10N-WIDGET). Translation strings the widget ships. Shape { <key>: { en: string, <locale>?: string, ... } } — `en` is REQUIRED per key. At install the host merges these into the tenant's localization dictionary under a per-widget namespace (`widget.<manifest.id>.<key>`); `useI18n().t(\"<key>\")` resolves the namespaced key automatically, so authors never type the prefix. Non-destructive: an existing admin-edited value is never overwritten, and a language the tenant has not added is never created (values for absent locales are dropped; the widget's `en` seeds the tenant's base language so every key renders). Keys persist across uninstalls; admins prune them from the Translations admin (bulk delete by `widget.<id>.` prefix). Limits: ≤
|
|
4219
|
+
"Optional (REQ-L10N-WIDGET). Translation strings the widget ships. Shape { <key>: { en: string, <locale>?: string, ... } } — `en` is REQUIRED per key. At install the host merges these into the tenant's localization dictionary under a per-widget namespace (`widget.<manifest.id>.<key>`); `useI18n().t(\"<key>\")` resolves the namespaced key automatically, so authors never type the prefix. Non-destructive: an existing admin-edited value is never overwritten, and a language the tenant has not added is never created (values for absent locales are dropped; the widget's `en` seeds the tenant's base language so every key renders). Keys persist across uninstalls; admins prune them from the Translations admin (bulk delete by `widget.<id>.` prefix). Limits: ≤500 keys; each key matches /^[A-Za-z][A-Za-z0-9_.-]{0,63}$/; each value ≤1 KB; the namespaced key must fit the dictionary key cap (128 chars).",
|
|
4180
4220
|
default: {},
|
|
4181
4221
|
},
|
|
4182
4222
|
};
|
|
@@ -4342,6 +4382,17 @@ const WIDGET_CONTEXT_SHAPE = {
|
|
|
4342
4382
|
required: false,
|
|
4343
4383
|
fields: { run: "function", status: "function" },
|
|
4344
4384
|
},
|
|
4385
|
+
connectors: {
|
|
4386
|
+
description:
|
|
4387
|
+
"sc-15659 — injected connectors client from @colixsystems/actions-client. " +
|
|
4388
|
+
"{ call(connectorId, operation, { params, query, body }) -> Promise<{ status, content_type, body }> }. " +
|
|
4389
|
+
"Backs useConnector(). Calls one operation the workspace listed on an app-callable " +
|
|
4390
|
+
"connector, as the signed-in caller; the credential is added server-side. Errors carry " +
|
|
4391
|
+
"the server's `code` and `status`. Absent on a host that brokers none, where " +
|
|
4392
|
+
"useConnector() degrades to available: false rather than throwing.",
|
|
4393
|
+
required: false,
|
|
4394
|
+
fields: { call: "function" },
|
|
4395
|
+
},
|
|
4345
4396
|
linkPreviews: {
|
|
4346
4397
|
description:
|
|
4347
4398
|
"Injected @colixsystems/link-preview-client instance. { get(url) -> Promise<{ url, title, description, site_name, image_url }> }. " +
|
|
@@ -6749,7 +6800,12 @@ const CONTRACT = deepFreeze({
|
|
|
6749
6800
|
// `order: { table_id, record_id }` the server marks paid on settlement,
|
|
6750
6801
|
// a template column may declare `paymentStatus: true`, and four new
|
|
6751
6802
|
// PaymentError codes.
|
|
6752
|
-
|
|
6803
|
+
// 1.237.0: widening — `manifest.translations` may declare up to 500 keys
|
|
6804
|
+
// (was 200).
|
|
6805
|
+
// 1.238.0: additive (sc-15659) — `useConnector(connectorRef)` + the
|
|
6806
|
+
// `connectors` host client + the `connectorRef` propertySchema type + the
|
|
6807
|
+
// `connectors.call:*` scope. A widget calls a REST connector's listed operations.
|
|
6808
|
+
version: "1.238.0",
|
|
6753
6809
|
sharedTranslationKeys: SHARED_TRANSLATION_KEYS,
|
|
6754
6810
|
hooks: HOOKS,
|
|
6755
6811
|
primitives: PRIMITIVES,
|
package/dist/contract.js
CHANGED
|
@@ -2295,6 +2295,46 @@ const HOOKS = [
|
|
|
2295
2295
|
requiredContextSlice: ["actions.run"],
|
|
2296
2296
|
scopes: ["actions.run:*"],
|
|
2297
2297
|
},
|
|
2298
|
+
{
|
|
2299
|
+
name: "useConnector",
|
|
2300
|
+
signature: "useConnector(connectorRef)",
|
|
2301
|
+
description:
|
|
2302
|
+
"sc-15659 — call one of the WORKSPACE's REST connectors (a configured " +
|
|
2303
|
+
"outside API) straight from the widget, and use the answer in the " +
|
|
2304
|
+
"widget. call(operation, { params, query, body }) names an OPERATION " +
|
|
2305
|
+
"the workspace listed for apps on that connector, never a method or a " +
|
|
2306
|
+
"path: the server builds the request from the listing and adds the " +
|
|
2307
|
+
"connector's credential, which never reaches the app. `params` fills " +
|
|
2308
|
+
"the operation's {name} path segments (one whole segment each, no " +
|
|
2309
|
+
"slashes); `query` may use only the names the operation lists; `body` " +
|
|
2310
|
+
"only where the operation takes one. call resolves with { status, " +
|
|
2311
|
+
"content_type, body } — the outside API's status (a non-2xx answer " +
|
|
2312
|
+
"still resolves: branch on status) and its parsed JSON or text body. " +
|
|
2313
|
+
"The whole call must finish within 250 ms, token exchange included: a " +
|
|
2314
|
+
"slower outside API is cut off and the call rejects with UPSTREAM_TIMEOUT. " +
|
|
2315
|
+
"May run on mount for a read operation; an operation that changes data " +
|
|
2316
|
+
"belongs in an event handler. `connectorRef` is a connector id an " +
|
|
2317
|
+
"author bound through a `connectorRef` property — when it is missing, " +
|
|
2318
|
+
"or the host brokers no connectors client, `available` is false and " +
|
|
2319
|
+
"call() rejects UNAVAILABLE; render a visible 'pick a connector' state, " +
|
|
2320
|
+
"NEVER a blank box. Only signed-in app users may call. ConnectorError.code: " +
|
|
2321
|
+
"OPERATION_NOT_ALLOWED (403), CONNECTOR_NOT_FOUND (404, also when the " +
|
|
2322
|
+
"connector is not callable from the app) and CONNECTOR_SETUP_INCOMPLETE " +
|
|
2323
|
+
"(409) will NOT clear on a retry, so show an 'isn't available' state; " +
|
|
2324
|
+
"RATE_LIMITED (429) will; INVALID_CONNECTOR_PARAMS (400) means an " +
|
|
2325
|
+
"argument the operation does not take; UPSTREAM_ERROR (502) and " +
|
|
2326
|
+
"UPSTREAM_TIMEOUT (504) mean the outside API failed; AUTH_REQUIRED, " +
|
|
2327
|
+
"TOKEN_EXPIRED and TOKEN_INVALID mean the app user must sign in. NEVER " +
|
|
2328
|
+
"show an app user a raw error. Reads ctx.connectors.",
|
|
2329
|
+
returnShape: {
|
|
2330
|
+
available: "boolean // false when no connector is bound or the host brokers none",
|
|
2331
|
+
call: "(operation, { params?, query?, body? }) => Promise<{ status, content_type, body }> // rejects with ConnectorError",
|
|
2332
|
+
calling: "boolean // true while any call is in flight",
|
|
2333
|
+
error: "ConnectorError | null // the last call's error; cleared by the next call or a rebind",
|
|
2334
|
+
},
|
|
2335
|
+
requiredContextSlice: ["connectors.call"],
|
|
2336
|
+
scopes: ["connectors.call:*"],
|
|
2337
|
+
},
|
|
2298
2338
|
{
|
|
2299
2339
|
name: "useDatastoreMutation",
|
|
2300
2340
|
signature: "useDatastoreMutation(tableId)",
|
|
@@ -4172,7 +4212,7 @@ const MANIFEST_SCHEMA = {
|
|
|
4172
4212
|
type: "object",
|
|
4173
4213
|
required: false,
|
|
4174
4214
|
description:
|
|
4175
|
-
"Optional (REQ-L10N-WIDGET). Translation strings the widget ships. Shape { <key>: { en: string, <locale>?: string, ... } } — `en` is REQUIRED per key. At install the host merges these into the tenant's localization dictionary under a per-widget namespace (`widget.<manifest.id>.<key>`); `useI18n().t(\"<key>\")` resolves the namespaced key automatically, so authors never type the prefix. Non-destructive: an existing admin-edited value is never overwritten, and a language the tenant has not added is never created (values for absent locales are dropped; the widget's `en` seeds the tenant's base language so every key renders). Keys persist across uninstalls; admins prune them from the Translations admin (bulk delete by `widget.<id>.` prefix). Limits: ≤
|
|
4215
|
+
"Optional (REQ-L10N-WIDGET). Translation strings the widget ships. Shape { <key>: { en: string, <locale>?: string, ... } } — `en` is REQUIRED per key. At install the host merges these into the tenant's localization dictionary under a per-widget namespace (`widget.<manifest.id>.<key>`); `useI18n().t(\"<key>\")` resolves the namespaced key automatically, so authors never type the prefix. Non-destructive: an existing admin-edited value is never overwritten, and a language the tenant has not added is never created (values for absent locales are dropped; the widget's `en` seeds the tenant's base language so every key renders). Keys persist across uninstalls; admins prune them from the Translations admin (bulk delete by `widget.<id>.` prefix). Limits: ≤500 keys; each key matches /^[A-Za-z][A-Za-z0-9_.-]{0,63}$/; each value ≤1 KB; the namespaced key must fit the dictionary key cap (128 chars).",
|
|
4176
4216
|
default: {},
|
|
4177
4217
|
},
|
|
4178
4218
|
};
|
|
@@ -4338,6 +4378,17 @@ const WIDGET_CONTEXT_SHAPE = {
|
|
|
4338
4378
|
required: false,
|
|
4339
4379
|
fields: { run: "function", status: "function" },
|
|
4340
4380
|
},
|
|
4381
|
+
connectors: {
|
|
4382
|
+
description:
|
|
4383
|
+
"sc-15659 — injected connectors client from @colixsystems/actions-client. " +
|
|
4384
|
+
"{ call(connectorId, operation, { params, query, body }) -> Promise<{ status, content_type, body }> }. " +
|
|
4385
|
+
"Backs useConnector(). Calls one operation the workspace listed on an app-callable " +
|
|
4386
|
+
"connector, as the signed-in caller; the credential is added server-side. Errors carry " +
|
|
4387
|
+
"the server's `code` and `status`. Absent on a host that brokers none, where " +
|
|
4388
|
+
"useConnector() degrades to available: false rather than throwing.",
|
|
4389
|
+
required: false,
|
|
4390
|
+
fields: { call: "function" },
|
|
4391
|
+
},
|
|
4341
4392
|
linkPreviews: {
|
|
4342
4393
|
description:
|
|
4343
4394
|
"Injected @colixsystems/link-preview-client instance. { get(url) -> Promise<{ url, title, description, site_name, image_url }> }. " +
|
|
@@ -6745,7 +6796,12 @@ const CONTRACT = deepFreeze({
|
|
|
6745
6796
|
// `order: { table_id, record_id }` the server marks paid on settlement,
|
|
6746
6797
|
// a template column may declare `paymentStatus: true`, and four new
|
|
6747
6798
|
// PaymentError codes.
|
|
6748
|
-
|
|
6799
|
+
// 1.237.0: widening — `manifest.translations` may declare up to 500 keys
|
|
6800
|
+
// (was 200).
|
|
6801
|
+
// 1.238.0: additive (sc-15659) — `useConnector(connectorRef)` + the
|
|
6802
|
+
// `connectors` host client + the `connectorRef` propertySchema type + the
|
|
6803
|
+
// `connectors.call:*` scope. A widget calls a REST connector's listed operations.
|
|
6804
|
+
version: "1.238.0",
|
|
6749
6805
|
sharedTranslationKeys: SHARED_TRANSLATION_KEYS,
|
|
6750
6806
|
hooks: HOOKS,
|
|
6751
6807
|
primitives: PRIMITIVES,
|
package/dist/hooks.js
CHANGED
|
@@ -4118,6 +4118,115 @@ export function useAction(actionRef) {
|
|
|
4118
4118
|
};
|
|
4119
4119
|
}
|
|
4120
4120
|
|
|
4121
|
+
/**
|
|
4122
|
+
* sc-15659 — what useConnector() rejects with. `code` is one of:
|
|
4123
|
+
* - "UNAVAILABLE" — no connector bound, or the host brokers no
|
|
4124
|
+
* connectors client (render the unbound state)
|
|
4125
|
+
* - "OPERATION_NOT_ALLOWED" — 403 (the workspace did not list this operation)
|
|
4126
|
+
* - "INVALID_CONNECTOR_PARAMS" — 400 (an argument the operation does not take)
|
|
4127
|
+
* - "CONNECTOR_NOT_FOUND" — 404 (gone, or no longer callable from the app)
|
|
4128
|
+
* - "CONNECTOR_SETUP_INCOMPLETE" — 409 (the workspace has not finished its sign-in)
|
|
4129
|
+
* - "RATE_LIMITED" — 429 (too many calls — retry shortly)
|
|
4130
|
+
* - "AUTH_REQUIRED" / "TOKEN_EXPIRED" / "TOKEN_INVALID" — 401 (no app session)
|
|
4131
|
+
* - "UPSTREAM_ERROR" / "UPSTREAM_TIMEOUT" — 502 / 504 (the outside API failed)
|
|
4132
|
+
* - "CONNECTOR_FAILED" — no answer came
|
|
4133
|
+
*
|
|
4134
|
+
* The connectors client owns these codes; any other server code passes through.
|
|
4135
|
+
*/
|
|
4136
|
+
export class ConnectorError extends Error {
|
|
4137
|
+
constructor(code, message, opts) {
|
|
4138
|
+
super(message);
|
|
4139
|
+
this.name = "ConnectorError";
|
|
4140
|
+
this.code = code;
|
|
4141
|
+
if (opts && opts.cause) this.cause = opts.cause;
|
|
4142
|
+
}
|
|
4143
|
+
}
|
|
4144
|
+
|
|
4145
|
+
function toConnectorError(err) {
|
|
4146
|
+
if (err instanceof ConnectorError) return err;
|
|
4147
|
+
const code = err && typeof err.code === "string" && err.code ? err.code : "CONNECTOR_FAILED";
|
|
4148
|
+
const message = (err && err.message) || "The connector call failed";
|
|
4149
|
+
return new ConnectorError(code, message, { cause: err });
|
|
4150
|
+
}
|
|
4151
|
+
|
|
4152
|
+
/**
|
|
4153
|
+
* sc-15659 — call one of the workspace's REST connectors through an operation
|
|
4154
|
+
* the workspace listed for apps. Returns `{ available, call, calling, error }`.
|
|
4155
|
+
*
|
|
4156
|
+
* call(operation, { params?, query?, body? }) → Promise<{ status, content_type, body }>
|
|
4157
|
+
*
|
|
4158
|
+
* The widget owns what it does with the answer. A non-2xx upstream status
|
|
4159
|
+
* still resolves; a platform refusal rejects with a ConnectorError. The
|
|
4160
|
+
* connector's credential is added on the server and never reaches the app.
|
|
4161
|
+
*
|
|
4162
|
+
* `connectorRef` is the connector id an author bound through a
|
|
4163
|
+
* `connectorRef` property. When it is absent, or the host brokers no
|
|
4164
|
+
* connectors client, `available` is false and `call` rejects UNAVAILABLE
|
|
4165
|
+
* rather than throwing at render — render a visible unbound state.
|
|
4166
|
+
*
|
|
4167
|
+
* Routes through the injected `@colixsystems/actions-client` at
|
|
4168
|
+
* `ctx.connectors`, so the Player and the Expo export call identically.
|
|
4169
|
+
*/
|
|
4170
|
+
export function useConnector(connectorRef) {
|
|
4171
|
+
const ctx = useWidgetContextOrThrow("useConnector");
|
|
4172
|
+
const available =
|
|
4173
|
+
Boolean(connectorRef) && Boolean(ctx.connectors) && typeof ctx.connectors.call === "function";
|
|
4174
|
+
|
|
4175
|
+
const clientRef = useRef(null);
|
|
4176
|
+
clientRef.current = available ? ctx.connectors : null;
|
|
4177
|
+
const connectorIdRef = useRef(connectorRef);
|
|
4178
|
+
connectorIdRef.current = connectorRef;
|
|
4179
|
+
|
|
4180
|
+
const mountedRef = useRef(true);
|
|
4181
|
+
useEffect(() => {
|
|
4182
|
+
mountedRef.current = true;
|
|
4183
|
+
return () => {
|
|
4184
|
+
mountedRef.current = false;
|
|
4185
|
+
};
|
|
4186
|
+
}, []);
|
|
4187
|
+
|
|
4188
|
+
// A rebind starts clean: another connector's error never shows.
|
|
4189
|
+
const [outcome, setOutcome] = useState({ boundTo: connectorRef, pending: 0, error: null });
|
|
4190
|
+
|
|
4191
|
+
const call = useCallback(async (operation, args) => {
|
|
4192
|
+
const client = clientRef.current;
|
|
4193
|
+
const connectorId = connectorIdRef.current;
|
|
4194
|
+
if (!client || !connectorId) {
|
|
4195
|
+
throw new ConnectorError("UNAVAILABLE", "No connector is available for this host");
|
|
4196
|
+
}
|
|
4197
|
+
const settle = (error) => {
|
|
4198
|
+
if (!mountedRef.current) return;
|
|
4199
|
+
setOutcome((prior) =>
|
|
4200
|
+
prior.boundTo === connectorId
|
|
4201
|
+
? { boundTo: connectorId, pending: Math.max(0, prior.pending - 1), error }
|
|
4202
|
+
: prior,
|
|
4203
|
+
);
|
|
4204
|
+
};
|
|
4205
|
+
setOutcome((prior) =>
|
|
4206
|
+
prior.boundTo === connectorId
|
|
4207
|
+
? { ...prior, pending: prior.pending + 1, error: null }
|
|
4208
|
+
: { boundTo: connectorId, pending: 1, error: null },
|
|
4209
|
+
);
|
|
4210
|
+
try {
|
|
4211
|
+
const answer = await client.call(connectorId, operation, args);
|
|
4212
|
+
settle(null);
|
|
4213
|
+
return answer;
|
|
4214
|
+
} catch (err) {
|
|
4215
|
+
const e = toConnectorError(err);
|
|
4216
|
+
settle(e);
|
|
4217
|
+
throw e;
|
|
4218
|
+
}
|
|
4219
|
+
}, []);
|
|
4220
|
+
|
|
4221
|
+
const current = outcome.boundTo === connectorRef;
|
|
4222
|
+
return {
|
|
4223
|
+
available,
|
|
4224
|
+
call,
|
|
4225
|
+
calling: current && outcome.pending > 0,
|
|
4226
|
+
error: current ? outcome.error : null,
|
|
4227
|
+
};
|
|
4228
|
+
}
|
|
4229
|
+
|
|
4121
4230
|
/**
|
|
4122
4231
|
* Datastore mutation hook. Returns { create, update, delete }, each method
|
|
4123
4232
|
* returning a Promise. Routes through the injected
|
package/dist/index.d.ts
CHANGED
|
@@ -474,7 +474,7 @@ export interface WidgetManifest {
|
|
|
474
474
|
* under a per-widget namespace (`widget.<id>.<key>`), so `useI18n().t(key)`
|
|
475
475
|
* resolves the namespaced key automatically — the author never types the
|
|
476
476
|
* prefix. Non-destructive (admin edits win; absent languages are not
|
|
477
|
-
* created) and persists across uninstalls. Caps: ≤
|
|
477
|
+
* created) and persists across uninstalls. Caps: ≤500 keys, key matches
|
|
478
478
|
* /^[A-Za-z][A-Za-z0-9_.-]{0,63}$/, value ≤1 KB.
|
|
479
479
|
*/
|
|
480
480
|
translations?: Record<string, { en: string } & Record<string, string>>;
|
|
@@ -910,6 +910,10 @@ export interface WidgetContext<TProps = unknown> {
|
|
|
910
910
|
/** sc-14224 — whether a run would reach the script; optional on older hosts. */
|
|
911
911
|
status?(actionId: string): Promise<{ available: boolean; reason: string | null }>;
|
|
912
912
|
};
|
|
913
|
+
/** sc-15659 — injected @colixsystems/actions-client backing useConnector(). */
|
|
914
|
+
connectors?: {
|
|
915
|
+
call(connectorId: string, operation: string, args?: ConnectorCallArgs): Promise<ConnectorCallResult>;
|
|
916
|
+
};
|
|
913
917
|
/** Injected @colixsystems/link-preview-client backing useLinkPreview(). */
|
|
914
918
|
linkPreviews?: { get(url: string): Promise<LinkPreview> };
|
|
915
919
|
/** Host child-node renderer; backs WidgetTree / useChildRenderer. */
|
|
@@ -2105,6 +2109,40 @@ export interface ActionApi {
|
|
|
2105
2109
|
*/
|
|
2106
2110
|
export function useAction(actionRef: string | null | undefined): ActionApi;
|
|
2107
2111
|
|
|
2112
|
+
export interface ConnectorCallArgs {
|
|
2113
|
+
/** Values for the operation's `{name}` path segments; one whole segment each. */
|
|
2114
|
+
params?: Record<string, string | number>;
|
|
2115
|
+
/** Only the query names the operation lists. */
|
|
2116
|
+
query?: Record<string, string | number | boolean | Array<string | number | boolean>>;
|
|
2117
|
+
/** A JSON body, only for an operation that takes one. */
|
|
2118
|
+
body?: unknown;
|
|
2119
|
+
}
|
|
2120
|
+
|
|
2121
|
+
/** The outside API's answer: its status (non-2xx still resolves) and parsed body. */
|
|
2122
|
+
export interface ConnectorCallResult {
|
|
2123
|
+
status: number;
|
|
2124
|
+
content_type: string | null;
|
|
2125
|
+
body: unknown;
|
|
2126
|
+
}
|
|
2127
|
+
|
|
2128
|
+
export interface ConnectorApi {
|
|
2129
|
+
/** False when no connector is bound or the host brokers no connectors client. */
|
|
2130
|
+
available: boolean;
|
|
2131
|
+
/** Call one operation the workspace listed; rejects with ConnectorError. */
|
|
2132
|
+
call(operation: string, args?: ConnectorCallArgs): Promise<ConnectorCallResult>;
|
|
2133
|
+
/** True while any call is in flight. */
|
|
2134
|
+
calling: boolean;
|
|
2135
|
+
/** The last call's error; cleared by the next call or a rebind. */
|
|
2136
|
+
error: ConnectorError | null;
|
|
2137
|
+
}
|
|
2138
|
+
|
|
2139
|
+
/**
|
|
2140
|
+
* sc-15659 — call one of the workspace's REST connectors through an operation
|
|
2141
|
+
* the workspace listed for apps. The widget needs `connectors.call:*` in
|
|
2142
|
+
* `requestedScopes`; the credential never reaches the app.
|
|
2143
|
+
*/
|
|
2144
|
+
export function useConnector(connectorRef: string | null | undefined): ConnectorApi;
|
|
2145
|
+
|
|
2108
2146
|
// REQ-RT-07 realtime subscription transport state.
|
|
2109
2147
|
export type DatastoreSubscriptionStatus =
|
|
2110
2148
|
| "connecting"
|
|
@@ -3441,6 +3479,30 @@ export class AgentError extends Error {
|
|
|
3441
3479
|
* `RATE_LIMITED` will. `AUTH_REQUIRED`, `TOKEN_EXPIRED` and `TOKEN_INVALID`
|
|
3442
3480
|
* mean the app user must sign in. Never show an app user the raw message.
|
|
3443
3481
|
*/
|
|
3482
|
+
/**
|
|
3483
|
+
* sc-15659 — structured error thrown by `useConnector().call`. Branch on
|
|
3484
|
+
* `code`: `OPERATION_NOT_ALLOWED`, `CONNECTOR_NOT_FOUND` and
|
|
3485
|
+
* `CONNECTOR_SETUP_INCOMPLETE` will not clear on a retry, `RATE_LIMITED` will.
|
|
3486
|
+
* Never show an app user the raw message.
|
|
3487
|
+
*/
|
|
3488
|
+
export class ConnectorError extends Error {
|
|
3489
|
+
code:
|
|
3490
|
+
| "UNAVAILABLE"
|
|
3491
|
+
| "INVALID_CONNECTOR_PARAMS"
|
|
3492
|
+
| "OPERATION_NOT_ALLOWED"
|
|
3493
|
+
| "CONNECTOR_NOT_FOUND"
|
|
3494
|
+
| "CONNECTOR_SETUP_INCOMPLETE"
|
|
3495
|
+
| "RATE_LIMITED"
|
|
3496
|
+
| "AUTH_REQUIRED"
|
|
3497
|
+
| "TOKEN_EXPIRED"
|
|
3498
|
+
| "TOKEN_INVALID"
|
|
3499
|
+
| "UPSTREAM_ERROR"
|
|
3500
|
+
| "UPSTREAM_TIMEOUT"
|
|
3501
|
+
| "CONNECTOR_FAILED"
|
|
3502
|
+
| (string & {});
|
|
3503
|
+
constructor(code: string, message: string, opts?: { cause?: unknown });
|
|
3504
|
+
}
|
|
3505
|
+
|
|
3444
3506
|
export class ActionError extends Error {
|
|
3445
3507
|
code:
|
|
3446
3508
|
| "UNAVAILABLE"
|
package/dist/index.js
CHANGED
|
@@ -30,6 +30,7 @@ export {
|
|
|
30
30
|
PermissionError,
|
|
31
31
|
AgentError,
|
|
32
32
|
ActionError,
|
|
33
|
+
ConnectorError,
|
|
33
34
|
useDatastoreQuery,
|
|
34
35
|
useDatastoreRecord,
|
|
35
36
|
useDatastoreSchema,
|
|
@@ -39,6 +40,7 @@ export {
|
|
|
39
40
|
useInterpretDraft,
|
|
40
41
|
useAgent,
|
|
41
42
|
useAction,
|
|
43
|
+
useConnector,
|
|
42
44
|
useAsset,
|
|
43
45
|
useAssetsByTag,
|
|
44
46
|
useBundledAsset,
|
package/dist/index.native.js
CHANGED
|
@@ -30,6 +30,7 @@ export {
|
|
|
30
30
|
PermissionError,
|
|
31
31
|
AgentError,
|
|
32
32
|
ActionError,
|
|
33
|
+
ConnectorError,
|
|
33
34
|
useDatastoreQuery,
|
|
34
35
|
useDatastoreRecord,
|
|
35
36
|
useDatastoreSchema,
|
|
@@ -39,6 +40,7 @@ export {
|
|
|
39
40
|
useInterpretDraft,
|
|
40
41
|
useAgent,
|
|
41
42
|
useAction,
|
|
43
|
+
useConnector,
|
|
42
44
|
useAsset,
|
|
43
45
|
useAssetsByTag,
|
|
44
46
|
useBundledAsset,
|
package/dist/linter.cjs
CHANGED
|
@@ -861,6 +861,16 @@ function _scopeRules(source, manifest) {
|
|
|
861
861
|
snippet: "",
|
|
862
862
|
});
|
|
863
863
|
}
|
|
864
|
+
// sc-15659 — DISCLOSURE, as for useAction: the server gate is the app-user
|
|
865
|
+
// JWT plus the connector's app opt-in and listed operations.
|
|
866
|
+
if (/\buseConnector\s*\(/.test(source) && !declared.has("connectors.call:*")) {
|
|
867
|
+
findings.push({
|
|
868
|
+
rule: "scope-required-for-useConnector",
|
|
869
|
+
label: "useConnector() requires `connectors.call:*` in manifest.requestedScopes",
|
|
870
|
+
line: 0,
|
|
871
|
+
snippet: "",
|
|
872
|
+
});
|
|
873
|
+
}
|
|
864
874
|
const lines = source.split(/\r?\n/);
|
|
865
875
|
let firedUsersWrite = false;
|
|
866
876
|
let firedUsersDelete = false;
|
package/dist/linter.js
CHANGED
|
@@ -856,6 +856,16 @@ function _scopeRules(source, manifest) {
|
|
|
856
856
|
snippet: "",
|
|
857
857
|
});
|
|
858
858
|
}
|
|
859
|
+
// sc-15659 — DISCLOSURE, as for useAction: the server gate is the app-user
|
|
860
|
+
// JWT plus the connector's app opt-in and listed operations.
|
|
861
|
+
if (/\buseConnector\s*\(/.test(source) && !declared.has("connectors.call:*")) {
|
|
862
|
+
findings.push({
|
|
863
|
+
rule: "scope-required-for-useConnector",
|
|
864
|
+
label: "useConnector() requires `connectors.call:*` in manifest.requestedScopes",
|
|
865
|
+
line: 0,
|
|
866
|
+
snippet: "",
|
|
867
|
+
});
|
|
868
|
+
}
|
|
859
869
|
const lines = source.split(/\r?\n/);
|
|
860
870
|
let firedUsersWrite = false;
|
|
861
871
|
let firedUsersDelete = false;
|
package/dist/manifest.cjs
CHANGED
|
@@ -47,8 +47,8 @@ const VALID_PLATFORMS = new Set(["web", "native"]);
|
|
|
47
47
|
// backend/src/core/services/l10n.service.js). The locale pattern mirrors that
|
|
48
48
|
// service's LANG_CODE_RE so a manifest can't declare a locale the dictionary
|
|
49
49
|
// would reject.
|
|
50
|
-
//
|
|
51
|
-
const TRANSLATIONS_MAX_KEYS =
|
|
50
|
+
// Bounds one package's rows in a workspace's translation dictionary.
|
|
51
|
+
const TRANSLATIONS_MAX_KEYS = 500;
|
|
52
52
|
const TRANSLATION_VALUE_MAX_BYTES = 1024;
|
|
53
53
|
const TRANSLATION_KEY_RE = /^[A-Za-z][A-Za-z0-9_.-]{0,63}$/;
|
|
54
54
|
const TRANSLATION_LOCALE_RE = /^[a-z]{2,3}(-[A-Za-z0-9]{2,8})?$/;
|
package/dist/manifest.js
CHANGED
|
@@ -42,8 +42,8 @@ const VALID_PLATFORMS = new Set(["web", "native"]);
|
|
|
42
42
|
// backend/src/core/services/l10n.service.js). The locale pattern mirrors that
|
|
43
43
|
// service's LANG_CODE_RE so a manifest can't declare a locale the dictionary
|
|
44
44
|
// would reject.
|
|
45
|
-
//
|
|
46
|
-
const TRANSLATIONS_MAX_KEYS =
|
|
45
|
+
// Bounds one package's rows in a workspace's translation dictionary.
|
|
46
|
+
const TRANSLATIONS_MAX_KEYS = 500;
|
|
47
47
|
const TRANSLATION_VALUE_MAX_BYTES = 1024;
|
|
48
48
|
const TRANSLATION_KEY_RE = /^[A-Za-z][A-Za-z0-9_.-]{0,63}$/;
|
|
49
49
|
const TRANSLATION_LOCALE_RE = /^[a-z]{2,3}(-[A-Za-z0-9]{2,8})?$/;
|
package/dist/property-schema.js
CHANGED
|
@@ -32,6 +32,9 @@ const VALID_TYPES = new Set([
|
|
|
32
32
|
// sc-13843: `actionRef` picks one of the workspace's Actions, a bare
|
|
33
33
|
// Action UUID a widget passes to `useAction(actionRef)`. Remapped like agentRef.
|
|
34
34
|
"actionRef",
|
|
35
|
+
// sc-15659: `connectorRef` picks one of the workspace's app-callable REST
|
|
36
|
+
// connectors, a bare id a widget passes to `useConnector`. Remapped likewise.
|
|
37
|
+
"connectorRef",
|
|
35
38
|
// REQ-WDG-PAGEREF: `pageRef` is a page picker that emits a bare AppPage
|
|
36
39
|
// UUID into the page JSON. Renders a page `<select>` in the Studio
|
|
37
40
|
// Properties Panel; a widget reads it and navigates via
|
|
@@ -164,6 +167,7 @@ function coerceLeaf(def, value, path, errors) {
|
|
|
164
167
|
case "sectionRef":
|
|
165
168
|
case "agentRef":
|
|
166
169
|
case "actionRef":
|
|
170
|
+
case "connectorRef":
|
|
167
171
|
case "richText":
|
|
168
172
|
case "asset":
|
|
169
173
|
case "folderRef":
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@colixsystems/widget-sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.290.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
|
@@ -2295,6 +2295,46 @@ const HOOKS = [
|
|
|
2295
2295
|
requiredContextSlice: ["actions.run"],
|
|
2296
2296
|
scopes: ["actions.run:*"],
|
|
2297
2297
|
},
|
|
2298
|
+
{
|
|
2299
|
+
name: "useConnector",
|
|
2300
|
+
signature: "useConnector(connectorRef)",
|
|
2301
|
+
description:
|
|
2302
|
+
"sc-15659 — call one of the WORKSPACE's REST connectors (a configured " +
|
|
2303
|
+
"outside API) straight from the widget, and use the answer in the " +
|
|
2304
|
+
"widget. call(operation, { params, query, body }) names an OPERATION " +
|
|
2305
|
+
"the workspace listed for apps on that connector, never a method or a " +
|
|
2306
|
+
"path: the server builds the request from the listing and adds the " +
|
|
2307
|
+
"connector's credential, which never reaches the app. `params` fills " +
|
|
2308
|
+
"the operation's {name} path segments (one whole segment each, no " +
|
|
2309
|
+
"slashes); `query` may use only the names the operation lists; `body` " +
|
|
2310
|
+
"only where the operation takes one. call resolves with { status, " +
|
|
2311
|
+
"content_type, body } — the outside API's status (a non-2xx answer " +
|
|
2312
|
+
"still resolves: branch on status) and its parsed JSON or text body. " +
|
|
2313
|
+
"The whole call must finish within 250 ms, token exchange included: a " +
|
|
2314
|
+
"slower outside API is cut off and the call rejects with UPSTREAM_TIMEOUT. " +
|
|
2315
|
+
"May run on mount for a read operation; an operation that changes data " +
|
|
2316
|
+
"belongs in an event handler. `connectorRef` is a connector id an " +
|
|
2317
|
+
"author bound through a `connectorRef` property — when it is missing, " +
|
|
2318
|
+
"or the host brokers no connectors client, `available` is false and " +
|
|
2319
|
+
"call() rejects UNAVAILABLE; render a visible 'pick a connector' state, " +
|
|
2320
|
+
"NEVER a blank box. Only signed-in app users may call. ConnectorError.code: " +
|
|
2321
|
+
"OPERATION_NOT_ALLOWED (403), CONNECTOR_NOT_FOUND (404, also when the " +
|
|
2322
|
+
"connector is not callable from the app) and CONNECTOR_SETUP_INCOMPLETE " +
|
|
2323
|
+
"(409) will NOT clear on a retry, so show an 'isn't available' state; " +
|
|
2324
|
+
"RATE_LIMITED (429) will; INVALID_CONNECTOR_PARAMS (400) means an " +
|
|
2325
|
+
"argument the operation does not take; UPSTREAM_ERROR (502) and " +
|
|
2326
|
+
"UPSTREAM_TIMEOUT (504) mean the outside API failed; AUTH_REQUIRED, " +
|
|
2327
|
+
"TOKEN_EXPIRED and TOKEN_INVALID mean the app user must sign in. NEVER " +
|
|
2328
|
+
"show an app user a raw error. Reads ctx.connectors.",
|
|
2329
|
+
returnShape: {
|
|
2330
|
+
available: "boolean // false when no connector is bound or the host brokers none",
|
|
2331
|
+
call: "(operation, { params?, query?, body? }) => Promise<{ status, content_type, body }> // rejects with ConnectorError",
|
|
2332
|
+
calling: "boolean // true while any call is in flight",
|
|
2333
|
+
error: "ConnectorError | null // the last call's error; cleared by the next call or a rebind",
|
|
2334
|
+
},
|
|
2335
|
+
requiredContextSlice: ["connectors.call"],
|
|
2336
|
+
scopes: ["connectors.call:*"],
|
|
2337
|
+
},
|
|
2298
2338
|
{
|
|
2299
2339
|
name: "useDatastoreMutation",
|
|
2300
2340
|
signature: "useDatastoreMutation(tableId)",
|
|
@@ -4172,7 +4212,7 @@ const MANIFEST_SCHEMA = {
|
|
|
4172
4212
|
type: "object",
|
|
4173
4213
|
required: false,
|
|
4174
4214
|
description:
|
|
4175
|
-
"Optional (REQ-L10N-WIDGET). Translation strings the widget ships. Shape { <key>: { en: string, <locale>?: string, ... } } — `en` is REQUIRED per key. At install the host merges these into the tenant's localization dictionary under a per-widget namespace (`widget.<manifest.id>.<key>`); `useI18n().t(\"<key>\")` resolves the namespaced key automatically, so authors never type the prefix. Non-destructive: an existing admin-edited value is never overwritten, and a language the tenant has not added is never created (values for absent locales are dropped; the widget's `en` seeds the tenant's base language so every key renders). Keys persist across uninstalls; admins prune them from the Translations admin (bulk delete by `widget.<id>.` prefix). Limits: ≤
|
|
4215
|
+
"Optional (REQ-L10N-WIDGET). Translation strings the widget ships. Shape { <key>: { en: string, <locale>?: string, ... } } — `en` is REQUIRED per key. At install the host merges these into the tenant's localization dictionary under a per-widget namespace (`widget.<manifest.id>.<key>`); `useI18n().t(\"<key>\")` resolves the namespaced key automatically, so authors never type the prefix. Non-destructive: an existing admin-edited value is never overwritten, and a language the tenant has not added is never created (values for absent locales are dropped; the widget's `en` seeds the tenant's base language so every key renders). Keys persist across uninstalls; admins prune them from the Translations admin (bulk delete by `widget.<id>.` prefix). Limits: ≤500 keys; each key matches /^[A-Za-z][A-Za-z0-9_.-]{0,63}$/; each value ≤1 KB; the namespaced key must fit the dictionary key cap (128 chars).",
|
|
4176
4216
|
default: {},
|
|
4177
4217
|
},
|
|
4178
4218
|
};
|
|
@@ -4338,6 +4378,17 @@ const WIDGET_CONTEXT_SHAPE = {
|
|
|
4338
4378
|
required: false,
|
|
4339
4379
|
fields: { run: "function", status: "function" },
|
|
4340
4380
|
},
|
|
4381
|
+
connectors: {
|
|
4382
|
+
description:
|
|
4383
|
+
"sc-15659 — injected connectors client from @colixsystems/actions-client. " +
|
|
4384
|
+
"{ call(connectorId, operation, { params, query, body }) -> Promise<{ status, content_type, body }> }. " +
|
|
4385
|
+
"Backs useConnector(). Calls one operation the workspace listed on an app-callable " +
|
|
4386
|
+
"connector, as the signed-in caller; the credential is added server-side. Errors carry " +
|
|
4387
|
+
"the server's `code` and `status`. Absent on a host that brokers none, where " +
|
|
4388
|
+
"useConnector() degrades to available: false rather than throwing.",
|
|
4389
|
+
required: false,
|
|
4390
|
+
fields: { call: "function" },
|
|
4391
|
+
},
|
|
4341
4392
|
linkPreviews: {
|
|
4342
4393
|
description:
|
|
4343
4394
|
"Injected @colixsystems/link-preview-client instance. { get(url) -> Promise<{ url, title, description, site_name, image_url }> }. " +
|
|
@@ -6745,7 +6796,12 @@ const CONTRACT = deepFreeze({
|
|
|
6745
6796
|
// `order: { table_id, record_id }` the server marks paid on settlement,
|
|
6746
6797
|
// a template column may declare `paymentStatus: true`, and four new
|
|
6747
6798
|
// PaymentError codes.
|
|
6748
|
-
|
|
6799
|
+
// 1.237.0: widening — `manifest.translations` may declare up to 500 keys
|
|
6800
|
+
// (was 200).
|
|
6801
|
+
// 1.238.0: additive (sc-15659) — `useConnector(connectorRef)` + the
|
|
6802
|
+
// `connectors` host client + the `connectorRef` propertySchema type + the
|
|
6803
|
+
// `connectors.call:*` scope. A widget calls a REST connector's listed operations.
|
|
6804
|
+
version: "1.238.0",
|
|
6749
6805
|
sharedTranslationKeys: SHARED_TRANSLATION_KEYS,
|
|
6750
6806
|
hooks: HOOKS,
|
|
6751
6807
|
primitives: PRIMITIVES,
|