@colixsystems/widget-sdk 0.289.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 CHANGED
@@ -90,7 +90,11 @@ See the design reference for the full architecture: [`docs/architecture/widget-m
90
90
 
91
91
  ## Status
92
92
 
93
- `v0.289.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**.
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.
94
98
 
95
99
  ### What's new in 0.289.0 (contract 1.237.0)
96
100
 
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)",
@@ -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 }> }. " +
@@ -6751,7 +6802,10 @@ const CONTRACT = deepFreeze({
6751
6802
  // PaymentError codes.
6752
6803
  // 1.237.0: widening — `manifest.translations` may declare up to 500 keys
6753
6804
  // (was 200).
6754
- version: "1.237.0",
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",
6755
6809
  sharedTranslationKeys: SHARED_TRANSLATION_KEYS,
6756
6810
  hooks: HOOKS,
6757
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)",
@@ -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 }> }. " +
@@ -6747,7 +6798,10 @@ const CONTRACT = deepFreeze({
6747
6798
  // PaymentError codes.
6748
6799
  // 1.237.0: widening — `manifest.translations` may declare up to 500 keys
6749
6800
  // (was 200).
6750
- version: "1.237.0",
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",
6751
6805
  sharedTranslationKeys: SHARED_TRANSLATION_KEYS,
6752
6806
  hooks: HOOKS,
6753
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
@@ -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,
@@ -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;
@@ -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.289.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)",
@@ -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 }> }. " +
@@ -6747,7 +6798,10 @@ const CONTRACT = deepFreeze({
6747
6798
  // PaymentError codes.
6748
6799
  // 1.237.0: widening — `manifest.translations` may declare up to 500 keys
6749
6800
  // (was 200).
6750
- version: "1.237.0",
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",
6751
6805
  sharedTranslationKeys: SHARED_TRANSLATION_KEYS,
6752
6806
  hooks: HOOKS,
6753
6807
  primitives: PRIMITIVES,