@awesomate/sdk 0.6.0 → 0.8.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
@@ -79,6 +79,26 @@ Every failure is an `AwesomateError` with a `code`: `unauthenticated`, `forbidde
79
79
  The token is your account's hosting token. Use it in a server, a script or a scheduled job, never
80
80
  in a browser. For a browser, sign your app's own users in (below).
81
81
 
82
+ ## Your app's server: a key of its own
83
+
84
+ An app's own server, or an n8n workflow, should not carry the account's token. Give it a server
85
+ key instead: Claude makes one with `awesomate_crm_apps` (`create_key`), read or write, for every
86
+ kind or only the ones it names. The key is shown once; put it in the server's environment.
87
+
88
+ ```ts
89
+ const db = createClient({ token: process.env.AWESOMATE_APP_KEY! }); // ak_...
90
+ const { rows } = await db.query('job', { where: { status: 'booked' } });
91
+ await db.write('job', { status: 'done' }, { id: rows[0].id });
92
+ await db.call('book_job', { customer, title: 'Gutter clean', status: 'booked' });
93
+ ```
94
+
95
+ A key reads and writes records and runs saved queries and recipes, only for its kinds (a recipe
96
+ that writes anything else is refused). It reads every attribute of an app kind, and the contact
97
+ list only as Claude does. Writes need Support Plus and above. It cannot change kinds, rules,
98
+ queries or recipes, and it never works from a browser. Revoking a key stops it at once; switching
99
+ the app off stops all of them. From n8n, send it as `Authorization: Bearer ak_...` to
100
+ `https://hub.awesomate.ai/api/sdk/v1/server/rows/query` (and `/call`, `/queries/<key>/run`).
101
+
82
102
  ## Your app's users sign in (Pro and above)
83
103
 
84
104
  Set the app up first (Claude Code: `awesomate_crm_apps`): its name, the origins it runs on, and
@@ -126,6 +146,20 @@ and takes a fresh copy when it does. Up to 200 rows a list and 20 lists a connec
126
146
  `{ onRows, onError }` to hear about a list the server refused (a column that does not exist).
127
147
  Outside a browser, give it a WebSocket: `createAppClient({ publishableKey, WebSocket })`.
128
148
 
149
+ ### Staff find a customer
150
+
151
+ People in an app read no contacts but their own, so by default staff cannot put a job under a
152
+ customer. Switch it on per app for the roles that need it (`awesomate_crm_apps`,
153
+ `customer_lookup`, never the role customers sign in with). Those roles can then search by name or
154
+ email and point a record at any customer:
155
+
156
+ ```ts
157
+ const [jo] = await app.lookupCustomers({ q: 'jo' }); // or { ids: [...] }, up to 50
158
+ await app.write('job', { title: 'Gutters' }, { links: { customer: jo.id } });
159
+ ```
160
+
161
+ Name, email and phone only, and nothing the owner marked sensitive. Anyone else is refused.
162
+
129
163
  The rules are kept in your own database, which applies them to every read and write, so a mistake
130
164
  in the app cannot show anyone more than their role allows. A disabled person is refused on their
131
165
  next call. Sessions are kept in `localStorage`; in a Capacitor app pass `storage` (Preferences) and
package/dist/index.d.ts CHANGED
@@ -22,7 +22,7 @@
22
22
  * queries works on every plan; writing app data (kinds, records, saved queries, recipes) needs
23
23
  * crm:write, Support Plus and above.
24
24
  */
25
- export declare const VERSION = "0.6.0";
25
+ export declare const VERSION = "0.7.0";
26
26
  /** Augmented by the generated awesomate.d.ts, so each kind's rows are typed. */
27
27
  export interface Kinds {
28
28
  }
@@ -143,7 +143,11 @@ export declare class AwesomateError extends Error {
143
143
  serverCode?: string | undefined);
144
144
  }
145
145
  export interface ClientOptions {
146
- /** The account's hosting token (amt_pat_...) with crm:read. */
146
+ /**
147
+ * The account's hosting token (amt_pat_...) with crm:read, or an app's server key (ak_...) from
148
+ * the account's apps. A key reads and writes records and runs saved queries and recipes, for the
149
+ * kinds it was made for; changing kinds, queries or recipes needs the account's token.
150
+ */
147
151
  token: string;
148
152
  baseUrl?: string;
149
153
  fetch?: typeof fetch;
@@ -152,6 +156,8 @@ export declare class AwesomateClient {
152
156
  private readonly opts;
153
157
  private readonly base;
154
158
  private readonly doFetch;
159
+ /** An app key talks to its own routes, which carry only what a key may do. */
160
+ private readonly appKey;
155
161
  constructor(opts: ClientOptions);
156
162
  private request;
157
163
  /** The kinds this token can read, their columns, operators and examples. */
@@ -364,6 +370,13 @@ export interface AppUser {
364
370
  role: string;
365
371
  contact_id: string | null;
366
372
  }
373
+ /** A customer as lookupCustomers() returns them. A field the owner marked sensitive is null. */
374
+ export interface Customer {
375
+ id: string;
376
+ name: string | null;
377
+ email: string | null;
378
+ phone: string | null;
379
+ }
367
380
  export interface AppClientOptions {
368
381
  /** The app's publishable key (pk_...). Not a secret: it belongs in browser code. */
369
382
  publishableKey: string;
@@ -479,6 +492,16 @@ export declare class AwesomateAppClient {
479
492
  /** Create a record (returns its id) or change one with options.id, inside the kind's write rule for this user. */
480
493
  write<K extends KindName>(kind: K, data: Partial<RowOf<K>> | Record<string, unknown>, options?: WriteOptions): Promise<string>;
481
494
  archive<K extends KindName>(kind: K, id: string): Promise<void>;
495
+ /**
496
+ * Customers by name or email (two characters or more), or by id, so staff can find one and put
497
+ * a record under them. Only for a role the account lets look customers up in this app; anyone
498
+ * else gets a forbidden error. Name, email and phone only, at most 50.
499
+ */
500
+ lookupCustomers(options: {
501
+ q?: string;
502
+ ids?: string[];
503
+ limit?: number;
504
+ }): Promise<Customer[]>;
482
505
  /**
483
506
  * A list kept current: onRows gets the whole list (up to 200 rows, the query's order) at first
484
507
  * and again whenever a row in it is added, changed, removed, or stops being one this person may
package/dist/index.js CHANGED
@@ -22,7 +22,7 @@
22
22
  * queries works on every plan; writing app data (kinds, records, saved queries, recipes) needs
23
23
  * crm:write, Support Plus and above.
24
24
  */
25
- export const VERSION = '0.6.0';
25
+ export const VERSION = '0.7.0';
26
26
  const DEFAULT_BASE = 'https://hub.awesomate.ai';
27
27
  const ERROR_CODES = ['unauthenticated', 'forbidden', 'not_found', 'validation', 'consent_blocked', 'rate_limited', 'conflict', 'unavailable'];
28
28
  export class AwesomateError extends Error {
@@ -58,20 +58,37 @@ function errorCode(status, server) {
58
58
  return 'not_found';
59
59
  return 'unavailable';
60
60
  }
61
+ const ACCOUNT = '/api/my-crm/v1';
62
+ const KEY_ROUTES = [
63
+ ['GET', /^\/rows\/schema$/], ['POST', /^\/rows\/query$/], ['GET', /^\/rows\/[^/]+\/[^/?]+(\?.*)?$/], ['POST', /^\/call$/],
64
+ ['GET', /^\/queries$/], ['POST', /^\/queries\/[^/]+\/run$/], ['GET', /^\/recipes$/],
65
+ ];
66
+ /** The same call on an app key's routes, or a clear refusal for what only the account may do. */
67
+ function keyPath(method, path) {
68
+ const rest = path.startsWith(ACCOUNT) ? path.slice(ACCOUNT.length) : path;
69
+ if (KEY_ROUTES.some(([m, re]) => m === method && re.test(rest)))
70
+ return `/api/sdk/v1/server${rest}`;
71
+ throw new AwesomateError('forbidden', 'An app key reads and writes records and runs saved queries and recipes. This needs the account\'s token.', 0);
72
+ }
61
73
  export class AwesomateClient {
62
74
  opts;
63
75
  base;
64
76
  doFetch;
77
+ /** An app key talks to its own routes, which carry only what a key may do. */
78
+ appKey;
65
79
  constructor(opts) {
66
80
  this.opts = opts;
67
81
  if (!opts?.token)
68
- throw new Error('createClient needs a token (the account\'s hosting token with crm:read).');
82
+ throw new Error('createClient needs a token: the account\'s hosting token, or an app\'s server key (ak_...).');
83
+ this.appKey = opts.token.startsWith('ak_');
69
84
  this.base = (opts.baseUrl ?? DEFAULT_BASE).replace(/\/+$/, '');
70
85
  this.doFetch = opts.fetch ?? globalThis.fetch;
71
86
  if (!this.doFetch)
72
87
  throw new Error('No fetch available: use Node 18 or later, or pass fetch.');
73
88
  }
74
89
  async request(method, path, body, retry = true) {
90
+ if (this.appKey)
91
+ path = keyPath(method, path);
75
92
  const res = await this.doFetch(`${this.base}${path}`, {
76
93
  method,
77
94
  headers: {
@@ -497,6 +514,14 @@ export class AwesomateAppClient {
497
514
  async archive(kind, id) {
498
515
  await this.data('POST', '/call', { fn: 'archive_record', args: { kind, id } });
499
516
  }
517
+ /**
518
+ * Customers by name or email (two characters or more), or by id, so staff can find one and put
519
+ * a record under them. Only for a role the account lets look customers up in this app; anyone
520
+ * else gets a forbidden error. Name, email and phone only, at most 50.
521
+ */
522
+ async lookupCustomers(options) {
523
+ return (await this.data('POST', '/customers/lookup', options)).customers;
524
+ }
500
525
  /**
501
526
  * A list kept current: onRows gets the whole list (up to 200 rows, the query's order) at first
502
527
  * and again whenever a row in it is added, changed, removed, or stops being one this person may
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@awesomate/sdk",
3
- "version": "0.6.0",
3
+ "version": "0.8.0",
4
4
  "description": "Your own Awesomate data from Node and the browser: query contacts and app data with generated types, and sign your app's own users in",
5
5
  "license": "MIT",
6
6
  "type": "module",