@benmore/bm 2.7.310 → 2.7.312
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/index.d.ts +33 -2
- package/index.js +85 -41
- package/package.json +1 -1
package/index.d.ts
CHANGED
|
@@ -5,6 +5,10 @@ export interface User {
|
|
|
5
5
|
last_name?: string | null;
|
|
6
6
|
role: string;
|
|
7
7
|
verified: number;
|
|
8
|
+
/** Every role the user holds: global roles plus the current tenant's roles. */
|
|
9
|
+
roles?: string[];
|
|
10
|
+
/** A groups: app's current tenant id (an admin's act-as tenant while stepped in); null without one. */
|
|
11
|
+
group_id?: string | null;
|
|
8
12
|
[field: string]: unknown;
|
|
9
13
|
}
|
|
10
14
|
|
|
@@ -16,6 +20,16 @@ export interface ListOpts {
|
|
|
16
20
|
/** Column to order by (snake_case, as stored). */
|
|
17
21
|
sort?: string;
|
|
18
22
|
order?: 'asc' | 'desc';
|
|
23
|
+
/** REST shorthand for sort + order: 'starts_at:asc'. Ignored when sort is set. */
|
|
24
|
+
orderBy?: string;
|
|
25
|
+
/** Related rows to embed (?include=): 'author' or ['author', 'comments']. */
|
|
26
|
+
include?: string | string[];
|
|
27
|
+
/** Columns to return (?fields=). */
|
|
28
|
+
fields?: string | string[];
|
|
29
|
+
/** Page number for page/per_page pagination (list() still returns the rows only). */
|
|
30
|
+
page?: number;
|
|
31
|
+
per_page?: number;
|
|
32
|
+
/** Equality filters (col: value) and operator filters (col__gte: value). Other top-level keys are not sent. */
|
|
19
33
|
where?: Record<string, string | number | boolean>;
|
|
20
34
|
}
|
|
21
35
|
|
|
@@ -131,11 +145,28 @@ export interface RoomClient {
|
|
|
131
145
|
readonly raw: WebSocket;
|
|
132
146
|
}
|
|
133
147
|
|
|
148
|
+
/** POST /api/<table>/<id>/transition answer. */
|
|
149
|
+
export interface WorkflowTransitionResult {
|
|
150
|
+
status: 'transitioned';
|
|
151
|
+
from: string;
|
|
152
|
+
to: string;
|
|
153
|
+
/** The row id as it appeared in the URL (a string). */
|
|
154
|
+
id: string;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** GET /api/<table>/<id>/transitions answer: the row's state and the moves this caller may make. */
|
|
158
|
+
export interface WorkflowAvailable {
|
|
159
|
+
current: string;
|
|
160
|
+
/** role is set when the transition is role-gated (the caller holds it). */
|
|
161
|
+
transitions: Array<{ to: string; role?: string }>;
|
|
162
|
+
}
|
|
163
|
+
|
|
134
164
|
export interface WorkflowClient {
|
|
135
165
|
readonly table: string;
|
|
136
166
|
readonly id: number | string;
|
|
137
|
-
transitionTo(state: string): Promise<
|
|
138
|
-
available(): Promise<
|
|
167
|
+
transitionTo(state: string): Promise<WorkflowTransitionResult>;
|
|
168
|
+
available(): Promise<WorkflowAvailable>;
|
|
169
|
+
/** The state stored in the workflow's own field (workflows.yaml field:). */
|
|
139
170
|
current(): Promise<string>;
|
|
140
171
|
}
|
|
141
172
|
|
package/index.js
CHANGED
|
@@ -48,16 +48,30 @@ async function request(method, path, body, options) {
|
|
|
48
48
|
return { ok: res.ok, status: res.status, data };
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
+
// apiError builds the Error a failed request throws. The framework's JSON
|
|
52
|
+
// errors carry a short code in `error` and, when there is more to say, the
|
|
53
|
+
// explanation in `message` (a refused write, a flow that failed): the Error's
|
|
54
|
+
// message is that explanation, else the code, else fallback, and err.code is
|
|
55
|
+
// the code. err.status and err.body are the response as received.
|
|
56
|
+
function apiError(res, fallback) {
|
|
57
|
+
const data = res.data;
|
|
58
|
+
let text = fallback;
|
|
59
|
+
let code;
|
|
60
|
+
if (data && typeof data === 'object') {
|
|
61
|
+
if (typeof data.error === 'string' && data.error) { code = data.error; text = data.error; }
|
|
62
|
+
if (typeof data.message === 'string' && data.message) text = data.message;
|
|
63
|
+
}
|
|
64
|
+
const err = new Error(text);
|
|
65
|
+
err.status = res.status;
|
|
66
|
+
err.body = data;
|
|
67
|
+
if (code) err.code = code;
|
|
68
|
+
return err;
|
|
69
|
+
}
|
|
70
|
+
|
|
51
71
|
// Convenience: unwrap or throw. Most agent code wants this shape:
|
|
52
72
|
// const message = await bm.api.post('/api/messages', {...});
|
|
53
73
|
async function unwrap(res) {
|
|
54
|
-
if (!res.ok) {
|
|
55
|
-
const msg = res.data?.error || `HTTP ${res.status}`;
|
|
56
|
-
const err = new Error(msg);
|
|
57
|
-
err.status = res.status;
|
|
58
|
-
err.body = res.data;
|
|
59
|
-
throw err;
|
|
60
|
-
}
|
|
74
|
+
if (!res.ok) throw apiError(res, `HTTP ${res.status}`);
|
|
61
75
|
return res.data;
|
|
62
76
|
}
|
|
63
77
|
|
|
@@ -185,12 +199,7 @@ async function refreshMe() {
|
|
|
185
199
|
// Returns the user object on success; throws { status, body } on failure.
|
|
186
200
|
async function signIn(identifier, password) {
|
|
187
201
|
const res = await request('POST', '/login', { email: identifier, password });
|
|
188
|
-
if (!res.ok)
|
|
189
|
-
const err = new Error(res.data?.error || 'Invalid credentials');
|
|
190
|
-
err.status = res.status;
|
|
191
|
-
err.body = res.data;
|
|
192
|
-
throw err;
|
|
193
|
-
}
|
|
202
|
+
if (!res.ok) throw apiError(res, 'Invalid credentials');
|
|
194
203
|
return res.data?.user || null;
|
|
195
204
|
}
|
|
196
205
|
|
|
@@ -200,10 +209,7 @@ async function signIn(identifier, password) {
|
|
|
200
209
|
async function signUp(fields) {
|
|
201
210
|
const res = await request('POST', '/signup', fields);
|
|
202
211
|
if (!res.ok) {
|
|
203
|
-
|
|
204
|
-
err.status = res.status;
|
|
205
|
-
err.body = res.data;
|
|
206
|
-
throw err;
|
|
212
|
+
throw apiError(res, 'Sign-up failed');
|
|
207
213
|
}
|
|
208
214
|
return res.data?.user || null;
|
|
209
215
|
}
|
|
@@ -234,10 +240,7 @@ function _bufToB64u(buf) {
|
|
|
234
240
|
async function _passkeyCall(method, path, body, fallback) {
|
|
235
241
|
const res = await request(method, path, body);
|
|
236
242
|
if (!res.ok) {
|
|
237
|
-
|
|
238
|
-
err.status = res.status;
|
|
239
|
-
err.body = res.data;
|
|
240
|
-
throw err;
|
|
243
|
+
throw apiError(res, fallback);
|
|
241
244
|
}
|
|
242
245
|
return res.data;
|
|
243
246
|
}
|
|
@@ -368,6 +371,21 @@ function _seg(v) {
|
|
|
368
371
|
return e;
|
|
369
372
|
}
|
|
370
373
|
|
|
374
|
+
// list() options it sends. Anything else (a filter written at the top level
|
|
375
|
+
// instead of under where:, a misspelled option) never reaches the server, so
|
|
376
|
+
// say so once per table and key instead of returning unfiltered rows quietly.
|
|
377
|
+
const _LIST_OPTS = new Set(['limit', 'offset', 'cursor', 'q', 'sort', 'order', 'orderBy', 'order_by',
|
|
378
|
+
'include', 'fields', 'page', 'per_page', 'perPage', 'where']);
|
|
379
|
+
const _warnedListOpts = new Set();
|
|
380
|
+
function _warnUnknownListOpts(table, opts) {
|
|
381
|
+
for (const k of Object.keys(opts || {})) {
|
|
382
|
+
if (_LIST_OPTS.has(k) || _warnedListOpts.has(table + '\u0000' + k)) continue;
|
|
383
|
+
_warnedListOpts.add(table + '\u0000' + k);
|
|
384
|
+
console.warn(`bm.table('${table}').list(): option "${k}" is not sent. ` +
|
|
385
|
+
`Filters go under where: { ${k}: ... }; list options are ${[..._LIST_OPTS].join(', ')}.`);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
371
389
|
export function table(name) {
|
|
372
390
|
if (!name || typeof name !== 'string') {
|
|
373
391
|
throw new Error('bm.table(name) requires a string table name');
|
|
@@ -386,9 +404,21 @@ export function table(name) {
|
|
|
386
404
|
if (opts.q) qs.set('q', String(opts.q));
|
|
387
405
|
if (opts.sort) qs.set('sort', String(opts.sort));
|
|
388
406
|
if (opts.order) qs.set('order', String(opts.order));
|
|
407
|
+
// The REST list parameters under their REST names: orderBy=col:dir,
|
|
408
|
+
// include=rel,rel, page/per_page, fields=a,b. They used to be dropped
|
|
409
|
+
// here without a word, so the server never saw them.
|
|
410
|
+
const orderBy = opts.orderBy ?? opts.order_by;
|
|
411
|
+
if (orderBy) qs.set('orderBy', String(orderBy));
|
|
412
|
+
const csv = (v) => (Array.isArray(v) ? v.join(',') : String(v));
|
|
413
|
+
if (opts.include) qs.set('include', csv(opts.include));
|
|
414
|
+
if (opts.fields) qs.set('fields', csv(opts.fields));
|
|
415
|
+
if (opts.page != null) qs.set('page', String(opts.page));
|
|
416
|
+
const perPage = opts.per_page ?? opts.perPage;
|
|
417
|
+
if (perPage != null) qs.set('per_page', String(perPage));
|
|
389
418
|
if (opts.where) {
|
|
390
419
|
for (const [k, v] of Object.entries(opts.where)) qs.set(k, String(v));
|
|
391
420
|
}
|
|
421
|
+
_warnUnknownListOpts(t, opts);
|
|
392
422
|
const path = base + (qs.toString() ? '?' + qs : '');
|
|
393
423
|
const r = await api.get(path);
|
|
394
424
|
// Offset lists are bare arrays; cursor/page lists use { data: [...] }.
|
|
@@ -717,12 +747,22 @@ async function loadFlowsRegistry() {
|
|
|
717
747
|
return _flowsRegistry;
|
|
718
748
|
}
|
|
719
749
|
|
|
720
|
-
// Registry keys are camelCase (company_setup / company-setup → companySetup
|
|
721
|
-
//
|
|
722
|
-
//
|
|
723
|
-
//
|
|
750
|
+
// Registry keys are camelCase (company_setup / company-setup → companySetup;
|
|
751
|
+
// bm_types.go flowSDKName is the same rule). Accept snake_case and
|
|
752
|
+
// kebab-case spellings too - agents naturally write bm.flows.company_setup
|
|
753
|
+
// when the file is flows/company-setup.yaml, and making the obvious spelling
|
|
754
|
+
// work beats making them learn the mapping.
|
|
724
755
|
function camelizeFlowName(name) {
|
|
725
|
-
|
|
756
|
+
const parts = String(name).split(/[-_]+/).filter(Boolean);
|
|
757
|
+
if (!parts.length) return name;
|
|
758
|
+
return parts.map((p, i) => (i === 0 ? p.charAt(0).toLowerCase() : p.charAt(0).toUpperCase()) + p.slice(1)).join('');
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
// Own keys only: a name such as 'constructor' must not resolve through the
|
|
762
|
+
// prototype chain of the parsed registry.
|
|
763
|
+
function flowRegistryEntry(reg, name) {
|
|
764
|
+
const own = (k) => (Object.prototype.hasOwnProperty.call(reg, k) ? reg[k] : undefined);
|
|
765
|
+
return own(name) || own(camelizeFlowName(name));
|
|
726
766
|
}
|
|
727
767
|
|
|
728
768
|
export const flows = new Proxy({}, {
|
|
@@ -730,19 +770,20 @@ export const flows = new Proxy({}, {
|
|
|
730
770
|
if (typeof flowName !== 'string') return undefined;
|
|
731
771
|
return async (params = {}, body) => {
|
|
732
772
|
let reg = await loadFlowsRegistry();
|
|
733
|
-
let entry = reg
|
|
773
|
+
let entry = flowRegistryEntry(reg, flowName);
|
|
734
774
|
if (!entry) {
|
|
735
775
|
// The registry lists only flows THIS caller may call, so one loaded
|
|
736
776
|
// before sign-in (or before a role grant) can be missing the flow.
|
|
737
777
|
// Refetch once before reporting it as undeclared.
|
|
738
778
|
_flowsRegistry = null;
|
|
739
779
|
reg = await loadFlowsRegistry();
|
|
740
|
-
entry = reg
|
|
780
|
+
entry = flowRegistryEntry(reg, flowName);
|
|
741
781
|
}
|
|
742
782
|
if (!entry) {
|
|
743
783
|
const available = Object.keys(reg).sort();
|
|
784
|
+
const call = (n) => (/^[A-Za-z_$][\w$]*$/.test(n) ? `bm.flows.${n}()` : `bm.flows[${JSON.stringify(n)}]()`);
|
|
744
785
|
const listing = available.length
|
|
745
|
-
? `Available flows: ${available.map(
|
|
786
|
+
? `Available flows: ${available.map(call).join(', ')}.`
|
|
746
787
|
: 'No flows are registered (flows.yaml / flows/*.yaml define none with on.request triggers).';
|
|
747
788
|
throw new Error(
|
|
748
789
|
`bm.flows.${flowName} is not declared in flows.yaml. ${listing} ` +
|
|
@@ -757,11 +798,7 @@ export const flows = new Proxy({}, {
|
|
|
757
798
|
}
|
|
758
799
|
const method = (entry.method || 'POST').toUpperCase();
|
|
759
800
|
const res = await request(method, path, body);
|
|
760
|
-
if (!res.ok) {
|
|
761
|
-
const err = new Error(res.data?.error || `flow ${flowName} failed (${res.status})`);
|
|
762
|
-
err.status = res.status; err.body = res.data;
|
|
763
|
-
throw err;
|
|
764
|
-
}
|
|
801
|
+
if (!res.ok) throw apiError(res, `flow ${flowName} failed (${res.status})`);
|
|
765
802
|
// Async flows return {job_id, status_url} - wrap into a Job handle.
|
|
766
803
|
if (res.data && res.data.job_id && res.data.status_url) {
|
|
767
804
|
return makeJob(res.data.job_id, res.data.status_url);
|
|
@@ -786,12 +823,22 @@ export function workflow(table, id) {
|
|
|
786
823
|
return {
|
|
787
824
|
table,
|
|
788
825
|
id,
|
|
789
|
-
// Server expects { to: <newState> } (workflows.go handleTransition)
|
|
826
|
+
// Server expects { to: <newState> } (workflows.go handleTransition) and
|
|
827
|
+
// answers { status: 'transitioned', from, to, id }.
|
|
790
828
|
transitionTo: (state) => api.post(`${base}/transition`, { to: state }),
|
|
829
|
+
// { current, transitions: [{ to, role? }] } (handleAvailableTransitions).
|
|
791
830
|
available: () => api.get(`${base}/transitions`),
|
|
831
|
+
// The transitions endpoint reads the workflow's own field (workflows.yaml
|
|
832
|
+
// field:), whatever it is named; reading row.state ?? row.status missed
|
|
833
|
+
// any other field. It needs a signed-in caller, so an anonymous reader of
|
|
834
|
+
// a public table falls back to the row.
|
|
792
835
|
current: async () => {
|
|
793
|
-
const
|
|
794
|
-
|
|
836
|
+
const res = await request('GET', `${base}/transitions`);
|
|
837
|
+
if (res.status === 401) {
|
|
838
|
+
const row = await api.get(base);
|
|
839
|
+
return row?.state ?? row?.status;
|
|
840
|
+
}
|
|
841
|
+
return (await unwrap(res))?.current;
|
|
795
842
|
},
|
|
796
843
|
};
|
|
797
844
|
}
|
|
@@ -1119,10 +1166,7 @@ function waitForICEGathering(pc) {
|
|
|
1119
1166
|
async function broadcastSDPExchange(path, slug, sdpOffer) {
|
|
1120
1167
|
const res = await request('POST', path, { slug, sdp: sdpOffer });
|
|
1121
1168
|
if (!res.ok) {
|
|
1122
|
-
|
|
1123
|
-
err.status = res.status;
|
|
1124
|
-
err.body = res.data;
|
|
1125
|
-
throw err;
|
|
1169
|
+
throw apiError(res, 'broadcast: SDP exchange failed');
|
|
1126
1170
|
}
|
|
1127
1171
|
return res.data; // { sdp, session_id, slug }
|
|
1128
1172
|
}
|