primitive-admin 1.0.44 → 1.0.45

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.
Files changed (71) hide show
  1. package/README.md +43 -0
  2. package/assets/skill/skills/primitive-platform/SKILL.md +85 -26
  3. package/dist/bin/primitive.js +6 -0
  4. package/dist/bin/primitive.js.map +1 -1
  5. package/dist/src/commands/analytics.js +16 -16
  6. package/dist/src/commands/analytics.js.map +1 -1
  7. package/dist/src/commands/apps.js +14 -14
  8. package/dist/src/commands/apps.js.map +1 -1
  9. package/dist/src/commands/auth.js +70 -20
  10. package/dist/src/commands/auth.js.map +1 -1
  11. package/dist/src/commands/blob-buckets.js +11 -11
  12. package/dist/src/commands/blob-buckets.js.map +1 -1
  13. package/dist/src/commands/catalog.js +17 -17
  14. package/dist/src/commands/catalog.js.map +1 -1
  15. package/dist/src/commands/collection-type-configs.js +5 -5
  16. package/dist/src/commands/collection-type-configs.js.map +1 -1
  17. package/dist/src/commands/collections.js +6 -6
  18. package/dist/src/commands/collections.js.map +1 -1
  19. package/dist/src/commands/comparisons.js +6 -6
  20. package/dist/src/commands/comparisons.js.map +1 -1
  21. package/dist/src/commands/cron-triggers.js +17 -17
  22. package/dist/src/commands/cron-triggers.js.map +1 -1
  23. package/dist/src/commands/database-types.js +13 -13
  24. package/dist/src/commands/database-types.js.map +1 -1
  25. package/dist/src/commands/databases.js +132 -8
  26. package/dist/src/commands/databases.js.map +1 -1
  27. package/dist/src/commands/email-templates.js +6 -6
  28. package/dist/src/commands/email-templates.js.map +1 -1
  29. package/dist/src/commands/env.js +6 -6
  30. package/dist/src/commands/env.js.map +1 -1
  31. package/dist/src/commands/group-type-configs.js +6 -6
  32. package/dist/src/commands/group-type-configs.js.map +1 -1
  33. package/dist/src/commands/groups.js +7 -7
  34. package/dist/src/commands/groups.js.map +1 -1
  35. package/dist/src/commands/init.js +175 -144
  36. package/dist/src/commands/init.js.map +1 -1
  37. package/dist/src/commands/integrations.js +31 -21
  38. package/dist/src/commands/integrations.js.map +1 -1
  39. package/dist/src/commands/prompts.js +17 -16
  40. package/dist/src/commands/prompts.js.map +1 -1
  41. package/dist/src/commands/rule-sets.js +8 -8
  42. package/dist/src/commands/rule-sets.js.map +1 -1
  43. package/dist/src/commands/sync.js +803 -275
  44. package/dist/src/commands/sync.js.map +1 -1
  45. package/dist/src/commands/tokens.js +9 -9
  46. package/dist/src/commands/tokens.js.map +1 -1
  47. package/dist/src/commands/users.js +44 -3
  48. package/dist/src/commands/users.js.map +1 -1
  49. package/dist/src/commands/webhooks.js +18 -18
  50. package/dist/src/commands/webhooks.js.map +1 -1
  51. package/dist/src/commands/workflows.js +273 -63
  52. package/dist/src/commands/workflows.js.map +1 -1
  53. package/dist/src/lib/api-client.js +240 -72
  54. package/dist/src/lib/api-client.js.map +1 -1
  55. package/dist/src/lib/migration-nag.js +163 -0
  56. package/dist/src/lib/migration-nag.js.map +1 -0
  57. package/dist/src/lib/output.js +58 -6
  58. package/dist/src/lib/output.js.map +1 -1
  59. package/dist/src/lib/refresh-admin-credentials.js +103 -0
  60. package/dist/src/lib/refresh-admin-credentials.js.map +1 -0
  61. package/dist/src/lib/template.js +80 -1
  62. package/dist/src/lib/template.js.map +1 -1
  63. package/dist/src/lib/toml-database-config.js +384 -0
  64. package/dist/src/lib/toml-database-config.js.map +1 -0
  65. package/dist/src/lib/toml-params-validator.js +183 -0
  66. package/dist/src/lib/toml-params-validator.js.map +1 -0
  67. package/dist/src/lib/workflow-fragments.js +121 -0
  68. package/dist/src/lib/workflow-fragments.js.map +1 -0
  69. package/dist/src/lib/workflow-toml-validator.js +328 -0
  70. package/dist/src/lib/workflow-toml-validator.js.map +1 -0
  71. package/package.json +1 -1
@@ -1,26 +1,167 @@
1
1
  import { loadCredentials, saveCredentials, isTokenExpiringSoon, } from "./config.js";
2
2
  import { fetchWithTLS } from "./fetch.js";
3
3
  import { paginateAll } from "./paginate.js";
4
+ import { RefreshError, refreshAdminCredentials, } from "./refresh-admin-credentials.js";
4
5
  export class ApiError extends Error {
5
6
  statusCode;
6
7
  code;
7
- constructor(message, statusCode, code) {
8
+ /**
9
+ * Structured `details` payload from `corsErrorResponse`.
10
+ *
11
+ * The server's error envelope can emit `details` as either:
12
+ * - an array of validation issues (e.g. `[{ path, message }, ...]`) —
13
+ * this is what most legacy endpoints produce, and what sync.ts walks
14
+ * via `Array.isArray(err.details)` / `for (const detail of ...)`.
15
+ * - a record of structured offender fields (e.g. `{ refs, operations,
16
+ * opCount, line, column, ... }`) — emitted by the issue #666 schema
17
+ * gate and consumed by the typed exception subclasses below.
18
+ *
19
+ * Callers must narrow before use: `Array.isArray(err.details)` for the
20
+ * legacy shape, otherwise treat as `Record<string, any>`.
21
+ */
22
+ details;
23
+ constructor(message, statusCode, code, details) {
8
24
  super(message);
9
25
  this.statusCode = statusCode;
10
26
  this.code = code;
11
27
  this.name = "ApiError";
28
+ if (details !== undefined) {
29
+ this.details = details;
30
+ }
12
31
  }
13
32
  }
14
33
  export class ConflictError extends ApiError {
15
34
  serverModifiedAt;
16
35
  expectedModifiedAt;
17
- constructor(message, serverModifiedAt, expectedModifiedAt) {
18
- super(message, 409, "CONFLICT");
36
+ constructor(message, serverModifiedAt, expectedModifiedAt, details) {
37
+ super(message, 409, "CONFLICT", details);
19
38
  this.serverModifiedAt = serverModifiedAt;
20
39
  this.expectedModifiedAt = expectedModifiedAt;
21
40
  this.name = "ConflictError";
22
41
  }
23
42
  }
43
+ /**
44
+ * Extract a human-readable error message + structured fields from a non-OK
45
+ * HTTP response body. Single source of truth used by every error-handler call
46
+ * site in this file (see issue #684).
47
+ */
48
+ export function parseErrorResponse(response, text, path) {
49
+ // Empty body → fall back to status code.
50
+ if (!text) {
51
+ return { message: `HTTP ${response.status}` };
52
+ }
53
+ let errorData;
54
+ try {
55
+ errorData = JSON.parse(text);
56
+ }
57
+ catch {
58
+ // Non-JSON body. Surface the existing `<!DOCTYPE` special-case (an HTML
59
+ // 404 page from hitting the wrong path) so we don't regress the helpful
60
+ // "API endpoint not found" message at api-client.ts:343.
61
+ if (text.includes("<!DOCTYPE")) {
62
+ const where = path ? `: ${path}` : "";
63
+ return {
64
+ message: `API endpoint not found${where}. Make sure the server is running.`,
65
+ htmlNotFound: true,
66
+ };
67
+ }
68
+ // Other non-JSON bodies (e.g. plain-text 502 from a proxy) — surface the
69
+ // raw text so the operator at least sees what the server returned.
70
+ return { message: text };
71
+ }
72
+ // Server's standard envelope uses `error`; ConflictError + integrations
73
+ // proxy use `message`. Prefer `error` (more common), fall back to `message`.
74
+ const message = (typeof errorData?.error === "string" && errorData.error) ||
75
+ (typeof errorData?.message === "string" && errorData.message) ||
76
+ `HTTP ${response.status}`;
77
+ // Per issue #666 addendum A1, `code` may be at the top level or nested
78
+ // under `details.code` when the server's bespoke envelope didn't flatten.
79
+ const code = (typeof errorData?.code === "string" ? errorData.code : undefined) ??
80
+ (typeof errorData?.details?.code === "string"
81
+ ? errorData.details.code
82
+ : undefined);
83
+ // Accept either an array (legacy) or a plain object (#666 schema gate).
84
+ const details = Array.isArray(errorData?.details)
85
+ ? errorData.details
86
+ : errorData?.details && typeof errorData.details === "object"
87
+ ? errorData.details
88
+ : undefined;
89
+ return { message, code, details, raw: errorData };
90
+ }
91
+ /**
92
+ * Typed exception classes for the database-schema feature (issue #666).
93
+ * Each maps 1:1 to a server `code` value emitted from the op-edit or
94
+ * schema-edit gate. They all extend ApiError so existing catch-all paths
95
+ * continue to work; specialized catch blocks can branch on `instanceof`.
96
+ *
97
+ * Per round-2 addendum A1, `details` is always preserved so callers can
98
+ * extract structured offender lists (refs[], operations[], etc.).
99
+ */
100
+ export class SchemaRequiredError extends ApiError {
101
+ constructor(message, details) {
102
+ super(message, 422, "SCHEMA_REQUIRED", details);
103
+ this.name = "SchemaRequiredError";
104
+ }
105
+ }
106
+ function detailsRecord(details) {
107
+ return details && !Array.isArray(details) && typeof details === "object"
108
+ ? details
109
+ : undefined;
110
+ }
111
+ export class OperationRefError extends ApiError {
112
+ constructor(message, details) {
113
+ super(message, 422, "OPERATION_REFERENCES_UNDEFINED", details);
114
+ this.name = "OperationRefError";
115
+ }
116
+ get refs() {
117
+ const d = detailsRecord(this.details);
118
+ return Array.isArray(d?.refs) ? d.refs : [];
119
+ }
120
+ }
121
+ export class SchemaBreaksOpsError extends ApiError {
122
+ constructor(message, details) {
123
+ super(message, 422, "SCHEMA_BREAKS_OPERATIONS", details);
124
+ this.name = "SchemaBreaksOpsError";
125
+ }
126
+ get operations() {
127
+ const d = detailsRecord(this.details);
128
+ return Array.isArray(d?.operations) ? d.operations : [];
129
+ }
130
+ }
131
+ export class SchemaHasUncheckableOpsError extends ApiError {
132
+ constructor(message, details) {
133
+ super(message, 422, "SCHEMA_HAS_UNCHECKABLE_OPS", details);
134
+ this.name = "SchemaHasUncheckableOpsError";
135
+ }
136
+ get operations() {
137
+ const d = detailsRecord(this.details);
138
+ return Array.isArray(d?.operations) ? d.operations : [];
139
+ }
140
+ }
141
+ export class TomlParseError extends ApiError {
142
+ constructor(message, details) {
143
+ super(message, 400, "TOML_PARSE_ERROR", details);
144
+ this.name = "TomlParseError";
145
+ }
146
+ get line() {
147
+ const d = detailsRecord(this.details);
148
+ return typeof d?.line === "number" ? d.line : undefined;
149
+ }
150
+ get column() {
151
+ const d = detailsRecord(this.details);
152
+ return typeof d?.column === "number" ? d.column : undefined;
153
+ }
154
+ }
155
+ export class OpsExistError extends ApiError {
156
+ constructor(message, details) {
157
+ super(message, 409, "OPS_EXIST", details);
158
+ this.name = "OpsExistError";
159
+ }
160
+ get opCount() {
161
+ const d = detailsRecord(this.details);
162
+ return typeof d?.opCount === "number" ? d.opCount : 0;
163
+ }
164
+ }
24
165
  export class ApiClient {
25
166
  credentials = null;
26
167
  constructor() {
@@ -40,39 +181,24 @@ export class ApiClient {
40
181
  return this.credentials;
41
182
  }
42
183
  async refreshToken() {
43
- if (!this.credentials?.refreshToken) {
44
- throw new ApiError("No refresh token available. Please login again.", 401);
184
+ if (!this.credentials) {
185
+ throw new ApiError("Not logged in. Run 'primitive login' first.", 401);
45
186
  }
46
- const url = `${this.credentials.serverUrl}/admin/api/auth/refresh`;
47
187
  try {
48
- const headers = {
49
- "Content-Type": "application/json",
50
- };
51
- if (this.credentials.globalAdminAppId) {
52
- headers["X-Global-Admin-App-Id"] = this.credentials.globalAdminAppId;
53
- }
54
- const response = await fetchWithTLS(url, {
55
- method: "POST",
56
- headers,
57
- body: JSON.stringify({ refreshToken: this.credentials.refreshToken }),
58
- });
59
- if (!response.ok) {
60
- throw new ApiError("Token refresh failed. Please login again.", 401);
61
- }
62
- const data = await response.json();
63
- // Update credentials with new tokens
64
- this.credentials = {
65
- ...this.credentials,
66
- accessToken: data.accessToken || data.token,
67
- refreshToken: data.refreshToken || this.credentials.refreshToken,
68
- expiresAt: data.expiresAt,
69
- };
188
+ const updated = await refreshAdminCredentials(this.credentials);
189
+ this.credentials = updated;
70
190
  saveCredentials(this.credentials);
71
191
  }
72
- catch (error) {
73
- if (error instanceof ApiError)
74
- throw error;
75
- throw new ApiError("Token refresh failed. Please login again.", 401);
192
+ catch (err) {
193
+ if (err instanceof RefreshError) {
194
+ // Preserve historical behavior: ApiClient surfaces refresh failures
195
+ // as 401s regardless of whether the underlying cause was a network
196
+ // error or a server-side rejection. Callers that need a finer
197
+ // distinction (e.g. `primitive token`) consume RefreshError directly
198
+ // from refresh-admin-credentials.ts instead of going through here.
199
+ throw new ApiError("Token refresh failed. Please login again.", 401);
200
+ }
201
+ throw err;
76
202
  }
77
203
  }
78
204
  async request(path, options = {}) {
@@ -93,20 +219,42 @@ export class ApiClient {
93
219
  });
94
220
  const text = await response.text();
95
221
  if (!response.ok) {
96
- let errorData;
97
- try {
98
- errorData = JSON.parse(text);
222
+ const parsed = parseErrorResponse(response, text, path);
223
+ // Preserve the `<!DOCTYPE` → 404 ApiError shape (status forced to 404).
224
+ if (parsed.htmlNotFound) {
225
+ throw new ApiError(parsed.message, 404);
99
226
  }
100
- catch {
101
- if (text.includes("<!DOCTYPE")) {
102
- throw new ApiError(`API endpoint not found: ${path}. Make sure the server is running.`, 404);
103
- }
104
- errorData = { message: text || `HTTP ${response.status}` };
227
+ // Narrow details to the record shape for the issue #666 typed-exception
228
+ // dispatch below. Conflict metadata may live under `details.*` (canonical
229
+ // location per A1) or on the top-level envelope (legacy).
230
+ const detailsRecord = parsed.details && !Array.isArray(parsed.details)
231
+ ? parsed.details
232
+ : undefined;
233
+ const serverModifiedAt = detailsRecord?.serverModifiedAt ?? parsed.raw?.serverModifiedAt;
234
+ const expectedModifiedAt = detailsRecord?.expectedModifiedAt ?? parsed.raw?.expectedModifiedAt;
235
+ // Typed exceptions for the schema-feature (issue #666).
236
+ if (response.status === 409 && parsed.code === "CONFLICT") {
237
+ throw new ConflictError(parsed.message, serverModifiedAt, expectedModifiedAt, detailsRecord);
105
238
  }
106
- if (response.status === 409 && errorData?.code === "CONFLICT") {
107
- throw new ConflictError(errorData.message || "Resource conflict", errorData.serverModifiedAt, errorData.expectedModifiedAt);
239
+ if (response.status === 409 && parsed.code === "OPS_EXIST") {
240
+ throw new OpsExistError(parsed.message, detailsRecord);
108
241
  }
109
- throw new ApiError(errorData.message || `HTTP ${response.status}`, response.status, errorData.code);
242
+ if (response.status === 400 && parsed.code === "TOML_PARSE_ERROR") {
243
+ throw new TomlParseError(parsed.message, detailsRecord);
244
+ }
245
+ if (response.status === 422 && parsed.code === "SCHEMA_REQUIRED") {
246
+ throw new SchemaRequiredError(parsed.message, detailsRecord);
247
+ }
248
+ if (response.status === 422 && parsed.code === "OPERATION_REFERENCES_UNDEFINED") {
249
+ throw new OperationRefError(parsed.message, detailsRecord);
250
+ }
251
+ if (response.status === 422 && parsed.code === "SCHEMA_BREAKS_OPERATIONS") {
252
+ throw new SchemaBreaksOpsError(parsed.message, detailsRecord);
253
+ }
254
+ if (response.status === 422 && parsed.code === "SCHEMA_HAS_UNCHECKABLE_OPS") {
255
+ throw new SchemaHasUncheckableOpsError(parsed.message, detailsRecord);
256
+ }
257
+ throw new ApiError(parsed.message, response.status, parsed.code, parsed.details);
110
258
  }
111
259
  return text ? JSON.parse(text) : null;
112
260
  }
@@ -196,9 +344,29 @@ export class ApiClient {
196
344
  async mintTestJwt(appId, userId, role) {
197
345
  return this.post(`/admin/api/apps/${appId}/users/${userId}/mint-test-jwt`, role ? { role } : {});
198
346
  }
199
- async listUsers(appId) {
200
- const result = await this.get(`/app/${appId}/api/users`);
201
- return result?.items ?? result ?? [];
347
+ async rebuildUserSearchText(appId) {
348
+ return this.post(`/admin/api/apps/${appId}/users/rebuild-search-text`, {});
349
+ }
350
+ async listUsers(appId, params) {
351
+ const query = new URLSearchParams();
352
+ if (params?.name)
353
+ query.set("name", params.name);
354
+ if (params?.email)
355
+ query.set("email", params.email);
356
+ if (params?.userId)
357
+ query.set("userId", params.userId);
358
+ if (params?.limit)
359
+ query.set("limit", String(params.limit));
360
+ if (params?.cursor)
361
+ query.set("cursor", params.cursor);
362
+ const path = query.toString()
363
+ ? `/app/${appId}/api/users?${query.toString()}`
364
+ : `/app/${appId}/api/users`;
365
+ const result = await this.get(path);
366
+ return {
367
+ items: result?.items ?? (Array.isArray(result) ? result : []),
368
+ nextCursor: result?.nextCursor ?? null,
369
+ };
202
370
  }
203
371
  async removeUser(appId, userId) {
204
372
  return this.delete(`/app/${appId}/api/users/${userId}`);
@@ -305,6 +473,10 @@ export class ApiClient {
305
473
  const result = await this.get(`/admin/api/apps/${appId}/integrations/${integrationId}/logs`, params);
306
474
  return result?.items ?? [];
307
475
  }
476
+ async listWorkflowRunIntegrationLogs(appId, runId, params) {
477
+ const result = await this.get(`/admin/api/apps/${appId}/workflows/runs/${runId}/integration-logs`, params);
478
+ return result?.items ?? [];
479
+ }
308
480
  async listIntegrationSecrets(appId, integrationId, params) {
309
481
  const result = await this.get(`/admin/api/apps/${appId}/integrations/${integrationId}/secrets`, params);
310
482
  return result?.items ?? [];
@@ -540,14 +712,8 @@ export class ApiClient {
540
712
  });
541
713
  const text = await response.text();
542
714
  if (!response.ok) {
543
- let errorData;
544
- try {
545
- errorData = JSON.parse(text);
546
- }
547
- catch {
548
- errorData = { message: text || `HTTP ${response.status}` };
549
- }
550
- throw new ApiError(errorData.message || `HTTP ${response.status}`, response.status, errorData.code);
715
+ const parsed = parseErrorResponse(response, text, url);
716
+ throw new ApiError(parsed.message, response.status, parsed.code, parsed.details);
551
717
  }
552
718
  return text ? JSON.parse(text) : null;
553
719
  }
@@ -567,14 +733,8 @@ export class ApiClient {
567
733
  });
568
734
  if (!response.ok) {
569
735
  const text = await response.text();
570
- let errorData;
571
- try {
572
- errorData = JSON.parse(text);
573
- }
574
- catch {
575
- errorData = { message: text || `HTTP ${response.status}` };
576
- }
577
- throw new ApiError(errorData.message || `HTTP ${response.status}`, response.status, errorData.code);
736
+ const parsed = parseErrorResponse(response, text, url);
737
+ throw new ApiError(parsed.message, response.status, parsed.code, parsed.details);
578
738
  }
579
739
  const arrayBuffer = await response.arrayBuffer();
580
740
  return {
@@ -995,14 +1155,8 @@ export class ApiClient {
995
1155
  const response = await fetchWithTLS(url, { ...options, headers });
996
1156
  const text = await response.text();
997
1157
  if (!response.ok) {
998
- let errorData;
999
- try {
1000
- errorData = JSON.parse(text);
1001
- }
1002
- catch {
1003
- errorData = { message: text || `HTTP ${response.status}` };
1004
- }
1005
- throw new ApiError(errorData.message || `HTTP ${response.status}`, response.status, errorData.code);
1158
+ const parsed = parseErrorResponse(response, text, path);
1159
+ throw new ApiError(parsed.message, response.status, parsed.code, parsed.details);
1006
1160
  }
1007
1161
  return text ? JSON.parse(text) : null;
1008
1162
  }
@@ -1030,13 +1184,27 @@ export class ApiClient {
1030
1184
  async createDatabaseTypeConfig(appId, data) {
1031
1185
  return this.post(`/app/${appId}/api/databases/types`, data);
1032
1186
  }
1033
- async updateDatabaseTypeConfig(appId, databaseType, data, expectedModifiedAt) {
1187
+ async updateDatabaseTypeConfig(appId, databaseType, data, expectedModifiedAt, options) {
1034
1188
  const body = expectedModifiedAt ? { ...data, expectedModifiedAt } : data;
1035
- return this.patch(`/app/${appId}/api/databases/types/${encodeURIComponent(databaseType)}`, body);
1189
+ const qs = new URLSearchParams();
1190
+ if (options?.dryRun)
1191
+ qs.set("dryRun", "true");
1192
+ if (options?.acceptWarnings)
1193
+ qs.set("acceptWarnings", "true");
1194
+ const query = qs.toString();
1195
+ const path = `/app/${appId}/api/databases/types/${encodeURIComponent(databaseType)}${query ? `?${query}` : ""}`;
1196
+ return this.patch(path, body);
1036
1197
  }
1037
1198
  async deleteDatabaseTypeConfig(appId, databaseType) {
1038
1199
  return this.delete(`/app/${appId}/api/databases/types/${encodeURIComponent(databaseType)}`);
1039
1200
  }
1201
+ /**
1202
+ * Issue #666 Phase 3: ask the server to scaffold a TOML schema from
1203
+ * existing ops + DO field introspection. Read-only — does NOT persist.
1204
+ */
1205
+ async scaffoldDatabaseTypeSchema(appId, databaseType) {
1206
+ return this.post(`/app/${appId}/api/databases/types/${encodeURIComponent(databaseType)}/schema:scaffold`, {});
1207
+ }
1040
1208
  // ============================================
1041
1209
  // DATABASE TYPE OPERATIONS
1042
1210
  // ============================================