@assembly-js/app-foundation 0.14.3 → 0.14.5
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/CHANGELOG.md +6 -0
- package/CLAUDE.md +19 -4
- package/dist/scripts.js +219 -11
- package/package.json +2 -2
- package/skills/assembly-custom-fields/SKILL.md +10 -6
- package/skills/assembly-native-resources/SKILL.md +37 -1
package/CHANGELOG.md
CHANGED
|
@@ -19,6 +19,12 @@ A patch release never changes what an app's build requires; a minor release may
|
|
|
19
19
|
not by hand; see `changes/README.md`. CI fails a PR that changes a check script without a
|
|
20
20
|
`bump: minor` change file.
|
|
21
21
|
|
|
22
|
+
## 0.14.5
|
|
23
|
+
- [guidance] The build agent now knows the public API accepts `select` (one option key string), `checkbox` (JSON boolean) and `date` (`YYYY-MM-DD`) custom fields, and that only `multiSelect` takes an array.
|
|
24
|
+
|
|
25
|
+
## 0.14.4
|
|
26
|
+
- [guidance] A selected company now gets one message on its company channel instead of a new individual channel per client.
|
|
27
|
+
|
|
22
28
|
## 0.14.3
|
|
23
29
|
- [guidance] The build agent now sets up services that sign in with OAuth (such as Dropbox) with a connection that renews itself, explains every setup step up front, and never asks for a token in chat.
|
|
24
30
|
|
package/CLAUDE.md
CHANGED
|
@@ -29,7 +29,7 @@ Assembly-specific gotchas live in `.claude/skills/<name>/SKILL.md`. Invoke them
|
|
|
29
29
|
- `assembly-client-users` — customers in the Client Portal
|
|
30
30
|
- `assembly-companies` — many-to-many with clients; placeholder companies (excluded from `listCompanies` by default for these apps)
|
|
31
31
|
- `assembly-clients-companies` — REQUIRED when the app stores or scopes data by client or company: scoped tables carry BOTH a `clientId` and a `companyId` column and are filtered on the intersection (never one dimension); a client viewer always has an active `companyId`; `companyIds[]` builds pickers only and `client.companyId` is deprecated
|
|
32
|
-
- `assembly-custom-fields` — workspace-defined fields, multiSelect rules
|
|
32
|
+
- `assembly-custom-fields` — workspace-defined fields, select / multiSelect rules
|
|
33
33
|
<!-- feature:platform-mcp -->
|
|
34
34
|
- `assembly-platform-mcp` — provisioning workspace configuration at build time via the scoped `mcp__platform__` tools: create a custom field the app depends on instead of shipping an app that gates on missing workspace setup. Workspace configuration only — never client-facing data.
|
|
35
35
|
<!-- /feature:platform-mcp -->
|
|
@@ -210,6 +210,14 @@ If the user's prompt says "build a CRM," the answer is mostly **wire up the exis
|
|
|
210
210
|
<!-- generated: sdk-methods -->
|
|
211
211
|
Generated from the SDK's service classes. Methods are called flat (`assembly.listClients(...)`); every method takes a single object of the named parameters (`?` = optional). Read the `.d.ts` only for nested parameter shapes. Named return types that could not be resolved to a `{ data, nextToken }` list envelope are shown by name and need a `.d.ts` lookup.
|
|
212
212
|
|
|
213
|
+
**Apps**
|
|
214
|
+
|
|
215
|
+
| Method | Parameters | Returns |
|
|
216
|
+
|---|---|---|
|
|
217
|
+
| `listApps` | — | `apps_ListAppsResponse` |
|
|
218
|
+
| `retrieveApp` | `appId` | `apps_AppDetail` |
|
|
219
|
+
| `retrieveAppContract` | `appId`, `ifNoneMatch?` | `Record<string, any>` |
|
|
220
|
+
|
|
213
221
|
**Clients**
|
|
214
222
|
|
|
215
223
|
| Method | Parameters | Returns |
|
|
@@ -261,6 +269,8 @@ Generated from the SDK's service classes. Methods are called flat (`assembly.lis
|
|
|
261
269
|
|---|---|---|
|
|
262
270
|
| `listCustomFields` | `name?`, `entityType?` | `{ data: Array<customfields_ClientCustomField>, nextToken }` |
|
|
263
271
|
| `createCustomField` | `requestBody` | `customfields_createCustomFieldsResponse` |
|
|
272
|
+
| `deleteCustomField` | `id` | `api_ItemFields` |
|
|
273
|
+
| `updateCustomField` | `id`, `requestBody` | `customfields_ClientCustomField` |
|
|
264
274
|
| `listCustomFieldOptions` | `id`, `label?` | `{ data: Array<customfields_ClientCustomFieldOption>, nextToken }` |
|
|
265
275
|
|
|
266
276
|
**Events**
|
|
@@ -1057,7 +1067,12 @@ await assembly.updateClient({
|
|
|
1057
1067
|
|
|
1058
1068
|
For option-bearing fields, also call `assembly.listCustomFieldOptions({ id: pizzaField.id })` to get the valid option `key`s — never accept an arbitrary string from a `<select>` without validating it's one of those keys.
|
|
1059
1069
|
|
|
1060
|
-
**Critical:
|
|
1070
|
+
**Critical: there are two option-bearing types, `select` (pick one) and `multiSelect` (pick many), and they take different value shapes.** Check the field's `type` from `listCustomFields` before writing:
|
|
1071
|
+
|
|
1072
|
+
- **`select`: the value is ONE option key as a string** — `{ tier: 'gold' }`. Send `''` to clear. An array is rejected.
|
|
1073
|
+
- **`multiSelect`: the value is an array of option keys** (details below).
|
|
1074
|
+
|
|
1075
|
+
For `multiSelect`:
|
|
1061
1076
|
|
|
1062
1077
|
- **The value MUST be an array of option keys, even if you're only setting one.** Sending a scalar string for a multiSelect field returns `400 invalid custom field option <value> provided for custom field: <key>`. The platform-api's validator casts the value to `[]interface{}` first thing — if it's not an array, the entire value (e.g. `"pepperoni"`) gets reported as the invalid option.
|
|
1063
1078
|
- A "single-pick" UX (a `<select>` with one selected option) still maps to `[selectedKey]` — wrap the user's selection in a one-element array on every write.
|
|
@@ -1079,7 +1094,7 @@ Identifier is flexible: the validator accepts either the option's `key` OR its `
|
|
|
1079
1094
|
|
|
1080
1095
|
For genuinely multi-pick UIs (checkbox group, etc.), pass the full selected array: `[a.key, b.key]`.
|
|
1081
1096
|
|
|
1082
|
-
Keys in the `customFields` object are the field's `key` property (from `listCustomFields`). `multiSelect` fields take an array; `address` fields take a structured object (see the `assembly-custom-fields` skill — don't flatten it to a string, that drops city/region/postal code); every other field type takes a direct string value — stringify numbers
|
|
1097
|
+
Keys in the `customFields` object are the field's `key` property (from `listCustomFields`). `multiSelect` fields take an array and `select` fields a single option key string; `address` fields take a structured object (see the `assembly-custom-fields` skill — don't flatten it to a string, that drops city/region/postal code); `checkbox` fields take a real JSON boolean (`{ onboarded: true }`; the string `'true'` is rejected); `date` fields take a `YYYY-MM-DD` string (`''` clears); every other field type takes a direct string value — stringify numbers yourself (`{ mrr: '5000' }`, not `{ mrr: 5000 }`).
|
|
1083
1098
|
|
|
1084
1099
|
**Match the field by name with a case-insensitive substring of the user's wording.** Don't match on internal `key`. The user knows their fields by display name, not by key.
|
|
1085
1100
|
|
|
@@ -1227,7 +1242,7 @@ Use `AssemblyBridge.header.*` for breadcrumbs, CTAs, and actions menu. See `.cla
|
|
|
1227
1242
|
- ❌ Don't re-notify a still-true condition from a cron. Mark a recipient sent only after `createNotification` succeeds, and clear that record when the condition ends unless the alert is once per billing period; keep those successful outcomes until the period resets, even if usage dips below the threshold. See "Recurring conditions" in `assembly-notifications`.
|
|
1228
1243
|
- ❌ Don't use title case — see "Labels, copy, and dropdowns" above.
|
|
1229
1244
|
- ❌ Don't render raw IDs or pass a raw ID string as `value` on `ComboboxItem` — see "Select and Combobox — base-ui differences" above.
|
|
1230
|
-
- ❌ Don't write a scalar string to a custom field that has options. **
|
|
1245
|
+
- ❌ Don't write a scalar string to a custom field that has options. **A `multiSelect` field takes an array of option keys** (`{ favorite_pizza: ['pepperoni'] }`, never `{ favorite_pizza: 'pepperoni' }`); a `select` field takes a single option key string. Check the field's `type` first. A scalar string for a multiSelect causes a 400 with `invalid custom field option <value> provided`.
|
|
1231
1246
|
- ❌ Don't read `searchParams.token` anywhere — not even the entry route. Auth lives in the bearer cookie; `getAssembly()` / `getSessionIdentity()` read it. The `?token=` on first load is the middleware's to consume, once.
|
|
1232
1247
|
- ❌ Don't design desktop-down. The app must work at 320 px. Build layouts mobile-first and add complexity at wider breakpoints.
|
|
1233
1248
|
- ❌ Don't render user-provided text (filenames, URLs, names) in a flex or grid container without overflow protection. CSS Grid and Flexbox children default to `min-width: auto` — a long string expands every ancestor up to the nearest `overflow-hidden` or `min-w-0`. Add `min-w-0` to **every** flex/grid child in the chain from the constrained container (e.g. `DialogContent`) to the text element, and `truncate` on the text itself. Missing `min-w-0` on even one ancestor breaks the entire chain.
|
package/dist/scripts.js
CHANGED
|
@@ -8,6 +8,7 @@ var __export = (target, all) => {
|
|
|
8
8
|
var api_exports = {};
|
|
9
9
|
__export(api_exports, {
|
|
10
10
|
ApiError: () => ApiError,
|
|
11
|
+
AppsService: () => AppsService,
|
|
11
12
|
CancelError: () => CancelError,
|
|
12
13
|
CancelablePromise: () => CancelablePromise,
|
|
13
14
|
ClientsService: () => ClientsService,
|
|
@@ -44,6 +45,7 @@ __export(api_exports, {
|
|
|
44
45
|
WorkspacesService: () => WorkspacesService,
|
|
45
46
|
api_ClientItem: () => api_ClientItem,
|
|
46
47
|
api_InternalUserItem: () => api_InternalUserItem,
|
|
48
|
+
api_UpdateClientInput: () => api_UpdateClientInput,
|
|
47
49
|
appConnection_AppConnectionMembershipType: () => appConnection_AppConnectionMembershipType,
|
|
48
50
|
appConnection_AppConnectionType: () => appConnection_AppConnectionType,
|
|
49
51
|
app_instance_AppConnectInput: () => app_instance_AppConnectInput,
|
|
@@ -66,6 +68,7 @@ __export(api_exports, {
|
|
|
66
68
|
contracts_SignatureEventType: () => contracts_SignatureEventType,
|
|
67
69
|
customfields_ClientCustomField: () => customfields_ClientCustomField,
|
|
68
70
|
customfields_createCustomFieldInput: () => customfields_createCustomFieldInput,
|
|
71
|
+
customfields_patchCustomFieldRequest: () => customfields_patchCustomFieldRequest,
|
|
69
72
|
fields_models_VisibilityComparator: () => fields_models_VisibilityComparator,
|
|
70
73
|
fields_models_VisibilityRuleType: () => fields_models_VisibilityRuleType,
|
|
71
74
|
filechannel_CreateFileChannelInput: () => filechannel_CreateFileChannelInput,
|
|
@@ -265,6 +268,7 @@ var api_ClientItem;
|
|
|
265
268
|
(function(status2) {
|
|
266
269
|
status2["UNKNOWN"] = "unknown";
|
|
267
270
|
status2["DELETED"] = "deleted";
|
|
271
|
+
status2["ARCHIVED"] = "archived";
|
|
268
272
|
status2["ACTIVE"] = "active";
|
|
269
273
|
status2["NOT_INVITED"] = "notInvited";
|
|
270
274
|
status2["INVITED"] = "invited";
|
|
@@ -281,6 +285,16 @@ var api_InternalUserItem;
|
|
|
281
285
|
})(role = api_InternalUserItem2.role || (api_InternalUserItem2.role = {}));
|
|
282
286
|
})(api_InternalUserItem || (api_InternalUserItem = {}));
|
|
283
287
|
|
|
288
|
+
// node_modules/@assembly-js/node-sdk/dist/codegen/api/models/api_UpdateClientInput.js
|
|
289
|
+
var api_UpdateClientInput;
|
|
290
|
+
(function(api_UpdateClientInput2) {
|
|
291
|
+
let status;
|
|
292
|
+
(function(status2) {
|
|
293
|
+
status2["ARCHIVED"] = "archived";
|
|
294
|
+
status2["ACTIVE"] = "active";
|
|
295
|
+
})(status = api_UpdateClientInput2.status || (api_UpdateClientInput2.status = {}));
|
|
296
|
+
})(api_UpdateClientInput || (api_UpdateClientInput = {}));
|
|
297
|
+
|
|
284
298
|
// node_modules/@assembly-js/node-sdk/dist/codegen/api/models/app_instance_AppConnectInput.js
|
|
285
299
|
var app_instance_AppConnectInput;
|
|
286
300
|
(function(app_instance_AppConnectInput2) {
|
|
@@ -356,11 +370,15 @@ var audit_log_AuditEventType;
|
|
|
356
370
|
audit_log_AuditEventType2["EventClientUpdated"] = "clientUpdated";
|
|
357
371
|
audit_log_AuditEventType2["EventClientDeleted"] = "clientDeleted";
|
|
358
372
|
audit_log_AuditEventType2["EventClientInvited"] = "clientInvited";
|
|
373
|
+
audit_log_AuditEventType2["EventClientArchived"] = "clientArchived";
|
|
374
|
+
audit_log_AuditEventType2["EventClientRestored"] = "clientRestored";
|
|
359
375
|
audit_log_AuditEventType2["EventCompanyCreated"] = "companyCreated";
|
|
360
376
|
audit_log_AuditEventType2["EventCompanyUpdated"] = "companyUpdated";
|
|
361
377
|
audit_log_AuditEventType2["EventCompanyCustomFieldsUpdated"] = "companyCustomFieldsUpdated";
|
|
362
378
|
audit_log_AuditEventType2["EventCompanyDeleted"] = "companyDeleted";
|
|
363
379
|
audit_log_AuditEventType2["EventClientCustomFieldsUpdated"] = "clientCustomFieldsUpdated";
|
|
380
|
+
audit_log_AuditEventType2["EventCustomFieldUpdated"] = "customFieldUpdated";
|
|
381
|
+
audit_log_AuditEventType2["EventCustomFieldDeleted"] = "customFieldDeleted";
|
|
364
382
|
audit_log_AuditEventType2["EventFileCreated"] = "fileCreated";
|
|
365
383
|
audit_log_AuditEventType2["EventFileUpdated"] = "fileUpdated";
|
|
366
384
|
audit_log_AuditEventType2["EventFileDeleted"] = "fileDeleted";
|
|
@@ -408,12 +426,16 @@ var audit_log_AuditEventType;
|
|
|
408
426
|
audit_log_AuditEventType2["EventInternalUserDeleted"] = "internalUserDeleted";
|
|
409
427
|
audit_log_AuditEventType2["EventInternalUserAccessLevelChanged"] = "internalUserAccessLevelChanged";
|
|
410
428
|
audit_log_AuditEventType2["EventInternalUserClientVisibilitySet"] = "internalUserClientVisibilitySet";
|
|
429
|
+
audit_log_AuditEventType2["EventAssistantAccessUpdated"] = "assistantAccessUpdated";
|
|
411
430
|
audit_log_AuditEventType2["EventAPIKeyCreated"] = "apiKeyCreated";
|
|
412
431
|
audit_log_AuditEventType2["EventAPIKeyDeleted"] = "apiKeyDeleted";
|
|
413
432
|
audit_log_AuditEventType2["EventConnectorConnected"] = "connectorConnected";
|
|
414
433
|
audit_log_AuditEventType2["EventConnectorReconnected"] = "connectorReconnected";
|
|
415
434
|
audit_log_AuditEventType2["EventConnectorRevoked"] = "connectorRevoked";
|
|
416
435
|
audit_log_AuditEventType2["EventConnectorRevokedExternally"] = "connectorRevokedExternally";
|
|
436
|
+
audit_log_AuditEventType2["EventSiteCredentialCreated"] = "siteCredentialCreated";
|
|
437
|
+
audit_log_AuditEventType2["EventSiteCredentialUsed"] = "siteCredentialUsed";
|
|
438
|
+
audit_log_AuditEventType2["EventSiteCredentialDeleted"] = "siteCredentialDeleted";
|
|
417
439
|
audit_log_AuditEventType2["EventFormCreated"] = "formCreated";
|
|
418
440
|
audit_log_AuditEventType2["EventFormEdited"] = "formEdited";
|
|
419
441
|
audit_log_AuditEventType2["EventFormAssigned"] = "formAssigned";
|
|
@@ -429,6 +451,8 @@ var audit_log_AuditEventType;
|
|
|
429
451
|
audit_log_AuditEventType2["EventAppAccessDeleted"] = "appAccessDeleted";
|
|
430
452
|
audit_log_AuditEventType2["EventAppBuilderSecretUpdated"] = "appBuilderSecretUpdated";
|
|
431
453
|
audit_log_AuditEventType2["EventAppEventEmitted"] = "appEventEmitted";
|
|
454
|
+
audit_log_AuditEventType2["EventAppDataChanged"] = "appDataChanged";
|
|
455
|
+
audit_log_AuditEventType2["EventClientCallRiskAccepted"] = "clientCallRiskAccepted";
|
|
432
456
|
audit_log_AuditEventType2["EventQAAgentBearerMinted"] = "qaAgentBearerMinted";
|
|
433
457
|
audit_log_AuditEventType2["EventQAImpersonationTokenMinted"] = "qaImpersonationTokenMinted";
|
|
434
458
|
audit_log_AuditEventType2["EventQAPrincipalCreated"] = "qaPrincipalCreated";
|
|
@@ -613,6 +637,9 @@ var customfields_ClientCustomField;
|
|
|
613
637
|
type2["PHONE_NUMBER"] = "phoneNumber";
|
|
614
638
|
type2["ADDRESS"] = "address";
|
|
615
639
|
type2["MULTI_SELECT"] = "multiSelect";
|
|
640
|
+
type2["DATE"] = "date";
|
|
641
|
+
type2["CHECKBOX"] = "checkbox";
|
|
642
|
+
type2["SELECT"] = "select";
|
|
616
643
|
})(type = customfields_ClientCustomField2.type || (customfields_ClientCustomField2.type = {}));
|
|
617
644
|
})(customfields_ClientCustomField || (customfields_ClientCustomField = {}));
|
|
618
645
|
|
|
@@ -633,9 +660,35 @@ var customfields_createCustomFieldInput;
|
|
|
633
660
|
type2["PHONE_NUMBER"] = "phoneNumber";
|
|
634
661
|
type2["ADDRESS"] = "address";
|
|
635
662
|
type2["MULTI_SELECT"] = "multiSelect";
|
|
663
|
+
type2["DATE"] = "date";
|
|
664
|
+
type2["CHECKBOX"] = "checkbox";
|
|
665
|
+
type2["SELECT"] = "select";
|
|
636
666
|
})(type = customfields_createCustomFieldInput2.type || (customfields_createCustomFieldInput2.type = {}));
|
|
637
667
|
})(customfields_createCustomFieldInput || (customfields_createCustomFieldInput = {}));
|
|
638
668
|
|
|
669
|
+
// node_modules/@assembly-js/node-sdk/dist/codegen/api/models/customfields_patchCustomFieldRequest.js
|
|
670
|
+
var customfields_patchCustomFieldRequest;
|
|
671
|
+
(function(customfields_patchCustomFieldRequest2) {
|
|
672
|
+
let entityType;
|
|
673
|
+
(function(entityType2) {
|
|
674
|
+
entityType2["CLIENT"] = "client";
|
|
675
|
+
entityType2["COMPANY"] = "company";
|
|
676
|
+
})(entityType = customfields_patchCustomFieldRequest2.entityType || (customfields_patchCustomFieldRequest2.entityType = {}));
|
|
677
|
+
let type;
|
|
678
|
+
(function(type2) {
|
|
679
|
+
type2["TEXT"] = "text";
|
|
680
|
+
type2["NUMBER"] = "number";
|
|
681
|
+
type2["EMAIL"] = "email";
|
|
682
|
+
type2["URL"] = "url";
|
|
683
|
+
type2["PHONE_NUMBER"] = "phoneNumber";
|
|
684
|
+
type2["ADDRESS"] = "address";
|
|
685
|
+
type2["MULTI_SELECT"] = "multiSelect";
|
|
686
|
+
type2["DATE"] = "date";
|
|
687
|
+
type2["CHECKBOX"] = "checkbox";
|
|
688
|
+
type2["SELECT"] = "select";
|
|
689
|
+
})(type = customfields_patchCustomFieldRequest2.type || (customfields_patchCustomFieldRequest2.type = {}));
|
|
690
|
+
})(customfields_patchCustomFieldRequest || (customfields_patchCustomFieldRequest = {}));
|
|
691
|
+
|
|
639
692
|
// node_modules/@assembly-js/node-sdk/dist/codegen/api/models/fields_models_VisibilityComparator.js
|
|
640
693
|
var fields_models_VisibilityComparator;
|
|
641
694
|
(function(fields_models_VisibilityComparator2) {
|
|
@@ -1445,6 +1498,89 @@ var request = (config, options) => {
|
|
|
1445
1498
|
}));
|
|
1446
1499
|
};
|
|
1447
1500
|
|
|
1501
|
+
// node_modules/@assembly-js/node-sdk/dist/codegen/api/services/AppsService.js
|
|
1502
|
+
var AppsService = class {
|
|
1503
|
+
/**
|
|
1504
|
+
* List callable apps
|
|
1505
|
+
*
|
|
1506
|
+
* Lists the other apps installed in the workspace that the calling app may
|
|
1507
|
+
* call through the platform proxy, each with the hash of its published API
|
|
1508
|
+
* description.
|
|
1509
|
+
*
|
|
1510
|
+
* @example
|
|
1511
|
+
* await assembly.listApps();
|
|
1512
|
+
*/
|
|
1513
|
+
static listApps() {
|
|
1514
|
+
return request(OpenAPI, {
|
|
1515
|
+
method: "GET",
|
|
1516
|
+
url: "/v1/apps",
|
|
1517
|
+
errors: {
|
|
1518
|
+
401: `Missing or invalid credential`,
|
|
1519
|
+
403: `The credential is not an app's`
|
|
1520
|
+
}
|
|
1521
|
+
});
|
|
1522
|
+
}
|
|
1523
|
+
/**
|
|
1524
|
+
* Retrieve an app's operations
|
|
1525
|
+
*
|
|
1526
|
+
* Returns one app the caller may call with the operations the platform
|
|
1527
|
+
* proxy serves for it: method, path and the label from its API description.
|
|
1528
|
+
* Retrieve the app's contract for each operation's description, inputs and
|
|
1529
|
+
* outputs. A workspace member also sees the operations the app's API
|
|
1530
|
+
* registry declares, each with its description and input fields.
|
|
1531
|
+
*
|
|
1532
|
+
* @param appId The app's id
|
|
1533
|
+
*
|
|
1534
|
+
* @example
|
|
1535
|
+
* await assembly.retrieveApp({ appId: ... });
|
|
1536
|
+
*/
|
|
1537
|
+
static retrieveApp({ appId }) {
|
|
1538
|
+
return request(OpenAPI, {
|
|
1539
|
+
method: "GET",
|
|
1540
|
+
url: "/v1/apps/{appId}",
|
|
1541
|
+
path: {
|
|
1542
|
+
"appId": appId
|
|
1543
|
+
},
|
|
1544
|
+
errors: {
|
|
1545
|
+
401: `Missing or invalid credential`,
|
|
1546
|
+
403: `The credential is not an app's`,
|
|
1547
|
+
404: `app_not_found`
|
|
1548
|
+
}
|
|
1549
|
+
});
|
|
1550
|
+
}
|
|
1551
|
+
/**
|
|
1552
|
+
* Retrieve an app's API description
|
|
1553
|
+
*
|
|
1554
|
+
* Returns the OpenAPI document the app published. The ETag is the
|
|
1555
|
+
* contractHash from the apps list; send it back in If-None-Match to get a
|
|
1556
|
+
* 304 when it has not changed.
|
|
1557
|
+
*
|
|
1558
|
+
* @param appId The app's id
|
|
1559
|
+
* @param If-None-Match ETag of a copy the caller already holds
|
|
1560
|
+
*
|
|
1561
|
+
* @example
|
|
1562
|
+
* await assembly.retrieveAppContract({ appId: ... });
|
|
1563
|
+
*/
|
|
1564
|
+
static retrieveAppContract({ appId, ifNoneMatch }) {
|
|
1565
|
+
return request(OpenAPI, {
|
|
1566
|
+
method: "GET",
|
|
1567
|
+
url: "/v1/apps/{appId}/contract",
|
|
1568
|
+
path: {
|
|
1569
|
+
"appId": appId
|
|
1570
|
+
},
|
|
1571
|
+
headers: {
|
|
1572
|
+
"If-None-Match": ifNoneMatch
|
|
1573
|
+
},
|
|
1574
|
+
errors: {
|
|
1575
|
+
304: `The description has not changed`,
|
|
1576
|
+
401: `Missing or invalid credential`,
|
|
1577
|
+
403: `The credential is not an app's`,
|
|
1578
|
+
404: `app_not_found or contract_not_found`
|
|
1579
|
+
}
|
|
1580
|
+
});
|
|
1581
|
+
}
|
|
1582
|
+
};
|
|
1583
|
+
|
|
1448
1584
|
// node_modules/@assembly-js/node-sdk/dist/codegen/api/services/ClientsService.js
|
|
1449
1585
|
var ClientsService = class {
|
|
1450
1586
|
/**
|
|
@@ -1487,8 +1623,9 @@ var ClientsService = class {
|
|
|
1487
1623
|
/**
|
|
1488
1624
|
* Create a client
|
|
1489
1625
|
*
|
|
1490
|
-
* Creates a client. Requires givenName, familyName,
|
|
1491
|
-
* association (
|
|
1626
|
+
* Creates a client. Requires givenName, familyName, and a company
|
|
1627
|
+
* association (plus email, unless the workspace allows clients without one)
|
|
1628
|
+
* (companyId, or companyIds in multi-company portals). Set the
|
|
1492
1629
|
* `sendInvite=true` query param to email the client an invite on creation.
|
|
1493
1630
|
*
|
|
1494
1631
|
* @param sendInvite Send the client an invite email on creation
|
|
@@ -1564,11 +1701,14 @@ var ClientsService = class {
|
|
|
1564
1701
|
/**
|
|
1565
1702
|
* Update a client
|
|
1566
1703
|
*
|
|
1567
|
-
* Partially updates a client. Only givenName, familyName, companyId,
|
|
1568
|
-
* customFields are mutable; any other field in the body is
|
|
1569
|
-
* echoed back in the `X-Ignored-Fields` response header. Set
|
|
1704
|
+
* Partially updates a client. Only givenName, familyName, companyId,
|
|
1705
|
+
* customFields and status are mutable; any other field in the body is
|
|
1706
|
+
* ignored and echoed back in the `X-Ignored-Fields` response header. Set
|
|
1570
1707
|
* `sendInvite=true` to send (or resend) the client's invite when they are
|
|
1571
|
-
* not yet active.
|
|
1708
|
+
* not yet active. Set `status` to `archived` to archive the client or
|
|
1709
|
+
* `active` to restore it; send it on its own, without other fields. An
|
|
1710
|
+
* archived client rejects any other edit with a `client_archived` error
|
|
1711
|
+
* until it is restored.
|
|
1572
1712
|
*
|
|
1573
1713
|
* @param id Client ID
|
|
1574
1714
|
* @param sendInvite Send or resend the client's invite email
|
|
@@ -1591,7 +1731,9 @@ var ClientsService = class {
|
|
|
1591
1731
|
errors: {
|
|
1592
1732
|
400: `Invalid request body`,
|
|
1593
1733
|
401: `Missing or invalid API key`,
|
|
1734
|
+
403: `Client archiving is not available`,
|
|
1594
1735
|
404: `Client not found`,
|
|
1736
|
+
409: `Client is archived`,
|
|
1595
1737
|
500: `Unexpected server error`
|
|
1596
1738
|
}
|
|
1597
1739
|
});
|
|
@@ -2064,7 +2206,7 @@ var CustomFieldsService = class {
|
|
|
2064
2206
|
*
|
|
2065
2207
|
* Creates one or more custom fields for the portal. Each field requires a
|
|
2066
2208
|
* name, a type, and the entity it belongs to. Options may only be supplied
|
|
2067
|
-
* for multi-select fields.
|
|
2209
|
+
* for multi-select and select fields.
|
|
2068
2210
|
*
|
|
2069
2211
|
* @example
|
|
2070
2212
|
* await assembly.createCustomField({ requestBody: ... });
|
|
@@ -2082,6 +2224,62 @@ var CustomFieldsService = class {
|
|
|
2082
2224
|
}
|
|
2083
2225
|
});
|
|
2084
2226
|
}
|
|
2227
|
+
/**
|
|
2228
|
+
* Delete a custom field
|
|
2229
|
+
*
|
|
2230
|
+
* Deletes a custom field definition by its ID. Returns the deleted object
|
|
2231
|
+
* marker.
|
|
2232
|
+
*
|
|
2233
|
+
* @param id Custom field ID
|
|
2234
|
+
*
|
|
2235
|
+
* @example
|
|
2236
|
+
* await assembly.deleteCustomField({ id: ... });
|
|
2237
|
+
*/
|
|
2238
|
+
static deleteCustomField({ id }) {
|
|
2239
|
+
return request(OpenAPI, {
|
|
2240
|
+
method: "DELETE",
|
|
2241
|
+
url: "/v1/custom-fields/{id}",
|
|
2242
|
+
path: {
|
|
2243
|
+
"id": id
|
|
2244
|
+
},
|
|
2245
|
+
errors: {
|
|
2246
|
+
400: `Missing custom field ID`,
|
|
2247
|
+
401: `Missing or invalid API key`,
|
|
2248
|
+
404: `Custom field not found`,
|
|
2249
|
+
500: `Unexpected server error`
|
|
2250
|
+
}
|
|
2251
|
+
});
|
|
2252
|
+
}
|
|
2253
|
+
/**
|
|
2254
|
+
* Update a custom field
|
|
2255
|
+
*
|
|
2256
|
+
* Renames a custom field and/or replaces the option set of a multiSelect or
|
|
2257
|
+
* select custom field. A custom field's key, type, and entityType cannot be
|
|
2258
|
+
* changed; supplying a type or entityType that differs from the field's
|
|
2259
|
+
* current value is rejected.
|
|
2260
|
+
*
|
|
2261
|
+
* @param id Custom field ID
|
|
2262
|
+
*
|
|
2263
|
+
* @example
|
|
2264
|
+
* await assembly.updateCustomField({ id: ..., requestBody: ... });
|
|
2265
|
+
*/
|
|
2266
|
+
static updateCustomField({ id, requestBody }) {
|
|
2267
|
+
return request(OpenAPI, {
|
|
2268
|
+
method: "PATCH",
|
|
2269
|
+
url: "/v1/custom-fields/{id}",
|
|
2270
|
+
path: {
|
|
2271
|
+
"id": id
|
|
2272
|
+
},
|
|
2273
|
+
body: requestBody,
|
|
2274
|
+
mediaType: "application/json",
|
|
2275
|
+
errors: {
|
|
2276
|
+
400: `Invalid request body, attempted type/entityType change, or options provided for a field that has none`,
|
|
2277
|
+
401: `Missing or invalid API key`,
|
|
2278
|
+
404: `Custom field not found`,
|
|
2279
|
+
500: `Unexpected server error`
|
|
2280
|
+
}
|
|
2281
|
+
});
|
|
2282
|
+
}
|
|
2085
2283
|
/**
|
|
2086
2284
|
* List custom field options
|
|
2087
2285
|
*
|
|
@@ -4276,15 +4474,25 @@ var TokenService = class {
|
|
|
4276
4474
|
* Issues a short-lived, app-scoped bearer token using the app API key alone
|
|
4277
4475
|
* — no user session required. Intended for cron jobs and background workers
|
|
4278
4476
|
* that operate outside a user iframe. The token expires after one hour;
|
|
4279
|
-
* re-mint it from the API key on expiry.
|
|
4477
|
+
* re-mint it from the API key on expiry. An optional JSON body field
|
|
4478
|
+
* `parent` carries the viewer bearer of the person whose request the app is
|
|
4479
|
+
* serving. A valid parent makes the minted bearer carry a signed statement
|
|
4480
|
+
* of who that person is and which company is active, and caps its lifetime
|
|
4481
|
+
* at the parent's. A present but invalid parent is refused with
|
|
4482
|
+
* `parent_invalid` (403) or `parent_expired` (401); it is never ignored. A
|
|
4483
|
+
* parent that reached this app through another app's call extends that call
|
|
4484
|
+
* chain by this app. The minted bearer's permissions are the same with or
|
|
4485
|
+
* without a parent.
|
|
4280
4486
|
*
|
|
4281
4487
|
* @example
|
|
4282
|
-
* await assembly.offlineTokenIssue();
|
|
4488
|
+
* await assembly.offlineTokenIssue({ requestBody: ... });
|
|
4283
4489
|
*/
|
|
4284
|
-
static offlineTokenIssue() {
|
|
4490
|
+
static offlineTokenIssue({ requestBody }) {
|
|
4285
4491
|
return request(OpenAPI, {
|
|
4286
4492
|
method: "POST",
|
|
4287
4493
|
url: "/v1/token",
|
|
4494
|
+
body: requestBody,
|
|
4495
|
+
mediaType: "application/json",
|
|
4288
4496
|
errors: {
|
|
4289
4497
|
401: `App API key required`,
|
|
4290
4498
|
403: `Missing or invalid API key`,
|
|
@@ -4451,7 +4659,7 @@ var __awaiter3 = function(thisArg, _arguments, P, generator) {
|
|
|
4451
4659
|
});
|
|
4452
4660
|
};
|
|
4453
4661
|
var { OpenAPI: OpenAPI2 } = api_exports;
|
|
4454
|
-
var SDK_VERSION = "4.
|
|
4662
|
+
var SDK_VERSION = "4.3.0";
|
|
4455
4663
|
installOpenAPIProxy(OpenAPI2);
|
|
4456
4664
|
var CLASS_OWN_KEYS = /* @__PURE__ */ new Set(["length", "name", "prototype"]);
|
|
4457
4665
|
var SERVICE_NAME_PATTERN = /Service$/;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@assembly-js/app-foundation",
|
|
3
|
-
"version": "0.14.
|
|
3
|
+
"version": "0.14.5",
|
|
4
4
|
"description": "Versioned foundation helpers and build context for Assembly generated apps",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"files": [
|
|
@@ -52,7 +52,7 @@
|
|
|
52
52
|
},
|
|
53
53
|
"devDependencies": {
|
|
54
54
|
"@assembly-js/app-bridge": "2.0.0",
|
|
55
|
-
"@assembly-js/node-sdk": "4.
|
|
55
|
+
"@assembly-js/node-sdk": "4.3.0",
|
|
56
56
|
"@neondatabase/serverless": "^1.1.0",
|
|
57
57
|
"@types/bun": "^1.1.0",
|
|
58
58
|
"@types/node": "^22",
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: assembly-custom-fields
|
|
3
|
-
description: Use any time the app reads or writes a workspace-defined custom field on a Client or Company. Two non-negotiable rules — never hardcode the field key (workspace owners pick their own), and
|
|
3
|
+
description: Use any time the app reads or writes a workspace-defined custom field on a Client or Company. Two non-negotiable rules — never hardcode the field key (workspace owners pick their own), and multiSelect values are arrays while select values are a single option key string.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
## The two non-negotiables
|
|
7
7
|
|
|
8
8
|
1. **Never hardcode a field `key`.** Workspace owners pick their own keys (`favorite_pizza`, `fav_pizza`, `pizza`, anything). Wrong-guess reads silently return `undefined`; wrong-guess writes silently no-op. Discover dynamically by name.
|
|
9
|
-
2. **
|
|
9
|
+
2. **Match the value shape to the field `type`.** `select` (pick one) takes ONE option key as a string; `multiSelect` (pick many) takes an array of option keys, even from a single-pick UI. `checkbox` takes a real boolean and `date` a `YYYY-MM-DD` string. `address` takes a structured object (see below). Everything else is a string.
|
|
10
10
|
|
|
11
11
|
## Discovery (server-side)
|
|
12
12
|
|
|
@@ -30,17 +30,20 @@ const optionsRes = await assembly.listCustomFieldOptions({ id: pizzaField.id });
|
|
|
30
30
|
|
|
31
31
|
```ts
|
|
32
32
|
const value = (client.customFields as Record<string, unknown>)?.[pizzaField.key];
|
|
33
|
-
//
|
|
33
|
+
// select: an option key string (or undefined if unset). multiSelect: an option key array, e.g. ['pepperoni'], or undefined / [] if unset.
|
|
34
|
+
// checkbox: boolean (missing means unchecked). date: 'YYYY-MM-DD' string.
|
|
34
35
|
```
|
|
35
36
|
|
|
36
|
-
## Write —
|
|
37
|
+
## Write — multiSelect MUST be an array, even single-pick
|
|
37
38
|
|
|
38
39
|
```ts
|
|
39
40
|
await assembly.updateClient({
|
|
40
41
|
id: clientId,
|
|
41
42
|
requestBody: {
|
|
42
43
|
customFields: {
|
|
43
|
-
[pizzaField.key]: selectedKey ? [selectedKey] : [], // ← array, ALWAYS
|
|
44
|
+
[pizzaField.key]: selectedKey ? [selectedKey] : [], // ← multiSelect: array, ALWAYS
|
|
45
|
+
// select field: [tierField.key]: selectedKey ?? '' (one key string, '' clears)
|
|
46
|
+
// checkbox field: [flagField.key]: true (boolean) date field: [dateField.key]: '2026-11-19'
|
|
44
47
|
},
|
|
45
48
|
// ↑ Pass a plain object. Do NOT JSON.stringify() it.
|
|
46
49
|
},
|
|
@@ -81,6 +84,7 @@ Send the structured object — a bare string is accepted for backwards compatibi
|
|
|
81
84
|
## Edge cases
|
|
82
85
|
|
|
83
86
|
- **Multi-pick UI** (checkbox group): pass full array `[a.key, b.key]`.
|
|
84
|
-
- **Clearing a value**: empty array `[]
|
|
87
|
+
- **Clearing a value**: empty array `[]` for multiSelect; `''` for select and date; not `null` or `undefined`.
|
|
88
|
+
- **Checkbox** rejects the string `'true'`; send a JSON boolean.
|
|
85
89
|
- **Either `key` or `id` works** as the inner option value — use `key` by convention.
|
|
86
90
|
- Same shape applies to `createClient`, `createCompany`, `updateCompany`.
|
|
@@ -69,6 +69,41 @@ await assembly.sendMessage({
|
|
|
69
69
|
|
|
70
70
|
`retrieveMessageChannel({ id })` is for a channel id you already resolved this request — never for one typed into the source.
|
|
71
71
|
|
|
72
|
+
## A company is one channel
|
|
73
|
+
|
|
74
|
+
When the user selects a **company**, send one message on that company's channel. Do not expand the company into its clients, and do not create an individual channel per client. That fan-out opens a new 1:1 thread for every person in the company and delivers the message there instead of the company thread they asked for.
|
|
75
|
+
|
|
76
|
+
Individual channels are only for a recipient the user picked as a specific client. The same rule applies to file channels: a company attachment goes on the company file channel.
|
|
77
|
+
|
|
78
|
+
```ts
|
|
79
|
+
async function findOrCreateCompanyMessageChannel(
|
|
80
|
+
assembly: AssemblyAPI,
|
|
81
|
+
companyId: string,
|
|
82
|
+
): Promise<string> {
|
|
83
|
+
const { data } = await assembly.listMessageChannels({
|
|
84
|
+
companyId,
|
|
85
|
+
membershipType: 'company',
|
|
86
|
+
limit: 1,
|
|
87
|
+
});
|
|
88
|
+
// A 1:1 thread for someone at this company also carries companyId. Send only
|
|
89
|
+
// when the row is the company channel.
|
|
90
|
+
const existing = data?.find((channel) => channel.membershipType === 'company');
|
|
91
|
+
if (existing?.id) return existing.id;
|
|
92
|
+
|
|
93
|
+
// membershipType is a string enum the package root does not re-export, so a
|
|
94
|
+
// bare 'company' literal does not typecheck. Assert the request body.
|
|
95
|
+
const created = await assembly.createMessageChannel({
|
|
96
|
+
requestBody: { companyId, membershipType: 'company' } as Parameters<
|
|
97
|
+
AssemblyAPI['createMessageChannel']
|
|
98
|
+
>[0]['requestBody'],
|
|
99
|
+
});
|
|
100
|
+
if (!created.id) throw new Error('Company channel was not created');
|
|
101
|
+
return created.id;
|
|
102
|
+
}
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Pass `companyId` when creating an individual channel for a client who belongs to more than one company. A create with only `clientId` is rejected for those clients.
|
|
106
|
+
|
|
72
107
|
## Discovering a file channel
|
|
73
108
|
|
|
74
109
|
```ts
|
|
@@ -113,6 +148,7 @@ Render an explicit, honest empty state and stop — "The intake form hasn't been
|
|
|
113
148
|
| Matched on the id instead of a stable attribute | Same — an id is not stable across workspaces, a name is |
|
|
114
149
|
| Matched a form on `form.name` | Always undefined; the display name is `form.fields.name`, so nothing ever matches and the app renders its empty state forever |
|
|
115
150
|
| Fell back to a literal id when discovery found nothing | The feature appears to work and writes into the wrong place, or fails on a resource the workspace can't see |
|
|
151
|
+
| Expanded a selected company into one individual channel per client | The workspace gets a burst of new 1:1 threads, and the message never lands in the company channel |
|
|
116
152
|
| Used the Files module for app-private uploads | Attachments show up in the client's portal Files tab |
|
|
117
153
|
|
|
118
154
|
## Edge cases
|
|
@@ -121,5 +157,5 @@ Render an explicit, honest empty state and stop — "The intake form hasn't been
|
|
|
121
157
|
- **Every list method takes an options object**, even when you pass no filters — `listForms({})`, not `listForms()`.
|
|
122
158
|
- **List responses are paginated envelopes**, and `data` is nullable: destructure with a default and still index defensively (`const { data: forms = [] } = …; forms?.[0]`).
|
|
123
159
|
- **Narrow before you index.** Taking `[0]` off an unfiltered list is the same guess as a hardcoded id. Filter by `membershipType` (and `companyId` for a client in multiple companies); if two candidates genuinely remain, surface the choice rather than picking one.
|
|
124
|
-
- **
|
|
160
|
+
- **Look up the channel, then create it only if that company or client does not have one yet.** Use `membershipType: 'company'` with `companyId` for a company, and `membershipType: 'individual'` with `clientId` (plus `companyId` when the client is in multiple companies) for one client. The enum is not re-exported from the package root, so assert the request body as in the company example. Never turn a company selection into one individual channel per member.
|
|
125
161
|
- **Ids from the app bridge or a route param** are runtime values, not literals — those are fine to pass straight to `retrieve*`, after the usual auth and scope checks (see `assembly-auth`).
|