@k9o/core 1.2.0 → 1.3.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/src/org.ts CHANGED
@@ -7,36 +7,55 @@
7
7
  * across accessible orgs) — `client.org` is the singular bound-org
8
8
  * namespace with member / group / smtp / audit / gate sub-namespaces.
9
9
  *
10
- * Org-plane URL grammar: requests route against the
11
- * `<orgSlug>.api.k9o.io/` org subdomain WITHOUT the `@<projectSlug>/`
12
- * prefix used by project-plane requests. The gateway resolves the org
13
- * context from the subdomain.
10
+ * Org-plane URL grammar: requests go to the org's API host (`<orgSlug>.api.<apex>`) WITHOUT the
11
+ * `@<projectSlug>/` prefix used by project-plane requests. The gateway resolves the org context from the host.
14
12
  */
15
13
 
16
14
  import { request, type HttpClientConfig } from "./http.ts";
17
15
  import { orgPath } from "./url-grammar.ts";
18
16
  import type { Envelope } from "./envelope.ts";
19
17
  import type { LLMModelId } from "./compute/llms.ts";
18
+ import type { Org } from "./orgs.ts";
20
19
 
21
20
  // ----------------------------------------------------------------------
22
21
  // Types
23
22
  // ----------------------------------------------------------------------
24
23
 
25
24
  export type OrgMember = {
26
- /** `u_<base62>`. */
25
+ /** `usr_<base62>`. */
27
26
  userId: string;
27
+ orgId: string;
28
28
  email: string;
29
- /** Org-level role enum. */
29
+ displayName: string;
30
+ /** Org-level role. */
30
31
  role: "ORG_OWNER" | "ORG_ADMIN" | "ORG_BILLING_ADMIN" | "ORG_MEMBER";
31
- joinedAt: string;
32
- status: "active" | "invited" | "suspended";
32
+ invitedAt: string;
33
+ /** `null` until the invitation is accepted. */
34
+ acceptedAt: string | null;
35
+ status: "active" | "suspended";
36
+ /** `invited` until accepted, then the same as `status`. */
37
+ state: "invited" | "active" | "suspended";
38
+ suspendedAt: string | null;
39
+ };
40
+
41
+ /**
42
+ * Answer of `members.invite` when the email has no account yet (HTTP 202): an invitation was created
43
+ * instead of a member. `delivery` says whether the invitation email went out.
44
+ */
45
+ export type OrgInviteResult = {
46
+ invitation: { id: string; orgId: string; email: string; role: OrgMember["role"]; expiresAt: string };
47
+ delivery: "sent" | "not_configured" | "failed";
33
48
  };
34
49
 
35
50
  export type OrgGroup = {
36
51
  /** `grp_<base62>`. */
37
52
  id: string;
53
+ orgId: string;
38
54
  name: string;
39
- memberCount: number;
55
+ description: string | null;
56
+ /** Groups come from an identity provider. */
57
+ source: "idp";
58
+ sourceGroupId: string;
40
59
  createdAt: string;
41
60
  };
42
61
 
@@ -44,7 +63,7 @@ export type OrgRoleGrant = {
44
63
  /** `grt_<base62>`. */
45
64
  id: string;
46
65
  userId: string;
47
- role: "ORG_ADMIN" | "ORG_BILLING_ADMIN";
66
+ role: "ORG_OWNER" | "ORG_ADMIN" | "ORG_BILLING_ADMIN" | "ORG_AUDITOR" | "ORG_MEMBER";
48
67
  grantedBy: string;
49
68
  grantedAt: string;
50
69
  revokedAt: string | null;
@@ -53,74 +72,90 @@ export type OrgRoleGrant = {
53
72
  };
54
73
 
55
74
  /**
56
- * Org-level audit event 6-event family.
57
- *
58
- * Event types (closed set at v2.1.132):
59
- * - `data.write.org.member_invited`
60
- * - `data.write.org.member_removed`
61
- * - `data.write.org.role_granted`
62
- * - `data.write.org.role_revoked`
63
- * - `data.write.org.idp_config_changed`
64
- * - `data.write.org.settings_updated`
65
- *
66
- * `targetKind` is `org` for all; `targetId` is the orgId.
75
+ * Org-level audit event: member invited or removed, role granted or revoked, identity provider
76
+ * or settings changed.
67
77
  */
68
78
  export type OrgAuditEvent = {
69
79
  id: string;
70
80
  type: string;
71
81
  actorId: string | null;
72
- targetKind: "org";
73
- targetId: string;
82
+ targetKind: string | null;
83
+ targetId: string | null;
74
84
  payload: Record<string, unknown>;
75
- occurredAt: string;
85
+ createdAt: string;
76
86
  requestId: string | null;
77
87
  traceId: string | null;
78
88
  };
79
89
 
80
90
  /**
81
- * Org SMTP configuration. Sensitive fields (passwordRef)
82
- * are sequestered in Kibo enclave; the SDK stores only the reference.
91
+ * Org SMTP configuration, as the gateway takes it in the `smtpConfig` field of PATCH /org.
92
+ * The password is never sent: `passwordRef` is a reference to a stored secret.
83
93
  */
84
94
  export type SmtpConfig = {
85
95
  host: string;
86
96
  port: number;
87
97
  username: string;
88
- /** Kibo enclave credential reference; opaque token. */
98
+ /** Reference to the stored SMTP password; opaque token. */
89
99
  passwordRef: string;
90
- fromEmail: string;
91
- fromName: string;
92
- encryption: "none" | "starttls" | "tls";
93
- replyToEmail?: string | undefined;
100
+ fromAddress: string;
101
+ fromName?: string | undefined;
94
102
  };
95
103
 
96
104
  export type SmtpTestEmailInput = {
97
105
  to: string;
98
- /** Optional override of the from address for the test send (defaults to fromEmail). */
99
- fromOverride?: string | undefined;
106
+ subject?: string | undefined;
100
107
  };
101
108
 
102
109
  export type SmtpTestEmailResult = {
103
- ok: boolean;
104
- /** Diagnostic message returned by the SMTP server (success or error detail). */
105
- diagnostic: string;
106
- /** ISO 8601 timestamp of the test attempt. */
107
- attemptedAt: string;
110
+ accepted: boolean;
111
+ /** Which SMTP configuration serves the org: `org` when it has its own, else `platform`. */
112
+ resolvedTier: "org" | "platform";
113
+ to: string;
114
+ /** `true` while the gateway does not send the message itself. */
115
+ dryRun: boolean;
116
+ };
117
+
118
+ export type OrgDefaultLlm = {
119
+ orgId: string;
120
+ defaultModelId: string;
121
+ updatedAt: string;
122
+ updatedBy: string | null;
108
123
  };
109
124
 
125
+ export type OrgIdpProviderKind =
126
+ | "google" | "github" | "microsoft" | "oidc" | "saml" | "scim" | "ldap" | "active_directory" | "apple" | "facebook" | "twitter";
127
+
110
128
  export type OrgIdpProvider = {
111
- /** `idp_<base62>`. */
112
129
  id: string;
113
- name: string;
114
- /** Protocol/vendor (e.g. "okta-saml", "azure-ad-oidc", "google-workspace"). */
115
- kind: string;
130
+ idpProvider: OrgIdpProviderKind;
131
+ displayName: string;
132
+ clientId: string;
116
133
  enabled: boolean;
117
134
  createdAt: string;
118
135
  };
119
136
 
137
+ /** Body of POST /identity-providers. `clientSecretRef` is a reference to a stored secret, never the secret itself. */
120
138
  export type CreateOrgIdpInput = {
121
- name: string;
122
- kind: string;
123
- config: Record<string, unknown>;
139
+ idpProvider: OrgIdpProviderKind;
140
+ displayName: string;
141
+ clientId: string;
142
+ clientSecretRef: string;
143
+ tierGating: "free_tier_ok" | "ridge_plus";
144
+ redirectUri?: string | undefined;
145
+ metadataUrl?: string | null | undefined;
146
+ attributeMap?: Record<string, unknown> | undefined;
147
+ enabled?: boolean | undefined;
148
+ };
149
+
150
+ /** Body of PATCH /identity-providers/<id>. */
151
+ export type UpdateOrgIdpInput = {
152
+ displayName?: string | undefined;
153
+ clientId?: string | undefined;
154
+ clientSecretRef?: string | undefined;
155
+ redirectUri?: string | undefined;
156
+ metadataUrl?: string | null | undefined;
157
+ attributeMap?: Record<string, unknown> | undefined;
158
+ enabled?: boolean | undefined;
124
159
  };
125
160
 
126
161
  // ----------------------------------------------------------------------
@@ -129,23 +164,24 @@ export type CreateOrgIdpInput = {
129
164
 
130
165
  function makeMembersNamespace(config: HttpClientConfig) {
131
166
  return {
132
- /** GET /members (org-plane LIST). */
133
- async list(opts?: { limit?: number | undefined; cursor?: string | undefined; status?: OrgMember["status"] | undefined }): Promise<Envelope<ReadonlyArray<OrgMember>>> {
167
+ /** GET /members (org-plane LIST). The route is not paged; it filters by userId, role, acceptanceStatus, status and state. */
168
+ async list(opts?: {
169
+ userId?: string | undefined; role?: OrgMember["role"] | undefined;
170
+ acceptanceStatus?: "accepted" | "pending" | undefined; status?: "active" | "suspended" | undefined; state?: string | undefined;
171
+ }): Promise<Envelope<ReadonlyArray<OrgMember>>> {
134
172
  return request(config, { method: "GET", path: orgPath("members"), query: opts });
135
173
  },
136
174
 
137
- /** POST /members — invite a new org member by email. */
138
- async invite(input: { email: string; role: OrgMember["role"] }): Promise<Envelope<OrgMember>> {
175
+ /**
176
+ * POST /members — add a member by email. An existing user becomes a member at once (201, the member row);
177
+ * an unknown email gets an invitation (202, `{ invitation, delivery }`).
178
+ */
179
+ async invite(input: { email: string; role: OrgMember["role"] }): Promise<Envelope<OrgMember | OrgInviteResult>> {
139
180
  return request(config, { method: "POST", path: orgPath("members"), body: input });
140
181
  },
141
182
 
142
- /** GET /members/<userId> — single-read (existing endpoint). */
143
- async get(userId: string): Promise<Envelope<OrgMember>> {
144
- return request(config, { method: "GET", path: orgPath(`members/${encodeURIComponent(userId)}`) });
145
- },
146
-
147
- /** DELETE /members/<userId> — remove a member from the org. */
148
- async remove(userId: string): Promise<Envelope<{ removed: boolean }>> {
183
+ /** DELETE /members/<userId> — remove a member from the org. The gateway answers 204 with no body. */
184
+ async remove(userId: string): Promise<Envelope<undefined>> {
149
185
  return request(config, { method: "DELETE", path: orgPath(`members/${encodeURIComponent(userId)}`) });
150
186
  },
151
187
  };
@@ -154,104 +190,78 @@ function makeMembersNamespace(config: HttpClientConfig) {
154
190
  function makeGroupsNamespace(config: HttpClientConfig) {
155
191
  return {
156
192
  /** GET /groups (org-plane LIST). */
157
- async list(opts?: { limit?: number; cursor?: string }): Promise<Envelope<ReadonlyArray<OrgGroup>>> {
193
+ async list(opts?: { userId?: string | undefined }): Promise<Envelope<ReadonlyArray<OrgGroup>>> {
158
194
  return request(config, { method: "GET", path: orgPath("groups"), query: opts });
159
195
  },
160
196
 
161
- /** POST /groups — create a new group. */
162
- async create(input: { name: string }): Promise<Envelope<OrgGroup>> {
163
- return request(config, { method: "POST", path: orgPath("groups"), body: input });
164
- },
165
-
166
- /** GET /groups/<groupId> — single-read (pre-cement existing endpoint). */
197
+ /** GET /groups/<groupId> — single-read. */
167
198
  async get(groupId: string): Promise<Envelope<OrgGroup>> {
168
199
  return request(config, { method: "GET", path: orgPath(`groups/${encodeURIComponent(groupId)}`) });
169
200
  },
170
201
 
171
- /** DELETE /groups/<groupId> — remove a group. */
172
- async delete(groupId: string): Promise<Envelope<{ deleted: boolean }>> {
173
- return request(config, { method: "DELETE", path: orgPath(`groups/${encodeURIComponent(groupId)}`) });
174
- },
175
202
  };
176
203
  }
177
204
 
178
205
  function makeOrgAuditLogNamespace(config: HttpClientConfig) {
179
206
  return {
180
207
  /**
181
- * GET /audit-log (org-plane).
182
- * Distinct from project-plane `client.audit.query()` which routes to
183
- * `@<projectSlug>/audit-log`. Org-plane events cover the 6-event family
184
- * (member_invited/removed/role_granted/revoked/
185
- * idp_config_changed/settings_updated).
208
+ * GET /audit-log (org-plane), newest first. The route reads `type` and `limit` (1 to 500, default 100) only.
209
+ * Distinct from project-plane `client.audit.query()` which routes to `@<projectSlug>/audit-log`.
186
210
  */
187
- async query(opts?: {
188
- since?: string | undefined; until?: string | undefined; type?: string | undefined; actorId?: string | undefined;
189
- targetKind?: string | undefined; targetId?: string | undefined; limit?: number | undefined; cursor?: string | undefined;
190
- }): Promise<Envelope<ReadonlyArray<OrgAuditEvent>>> {
211
+ async query(opts?: { type?: string | undefined; limit?: number | undefined }): Promise<Envelope<ReadonlyArray<OrgAuditEvent>>> {
191
212
  return request(config, { method: "GET", path: orgPath("audit-log"), query: opts });
192
213
  },
193
-
194
- /** Convenience: recent events (defaults to 5 minute window). */
195
- async tail(opts?: { sinceMinutes?: number; limit?: number }): Promise<Envelope<ReadonlyArray<OrgAuditEvent>>> {
196
- const minutes = opts?.sinceMinutes ?? 5;
197
- const since = new Date(Date.now() - minutes * 60_000).toISOString();
198
- return request(config, {
199
- method: "GET",
200
- path: orgPath("audit-log"),
201
- query: { since, limit: opts?.limit ?? 100 },
202
- });
203
- },
204
214
  };
205
215
  }
206
216
 
207
217
  function makeOrgGateNamespace(config: HttpClientConfig) {
208
218
  return {
209
219
  roles: {
210
- /** GET /org-role-grants — list active grants ask 4. */
211
- async list(opts?: { userId?: string | undefined; role?: OrgRoleGrant["role"] | undefined; includeRevoked?: boolean | undefined }): Promise<Envelope<ReadonlyArray<OrgRoleGrant>>> {
212
- return request(config, { method: "GET", path: orgPath("org-role-grants"), query: opts });
220
+ /** GET /role-grants — list role grants. */
221
+ async list(): Promise<Envelope<ReadonlyArray<OrgRoleGrant>>> {
222
+ return request(config, { method: "GET", path: orgPath("role-grants") });
213
223
  },
214
224
 
215
- /** POST /org-role-grants — grant an ORG_ADMIN or ORG_BILLING_ADMIN role. */
216
- async grant(input: { userId: string; role: OrgRoleGrant["role"] }): Promise<Envelope<OrgRoleGrant>> {
217
- return request(config, { method: "POST", path: orgPath("org-role-grants"), body: input });
225
+ /** POST /role-grants — grant an org role. */
226
+ async grant(input: { userId: string; role: OrgRoleGrant["role"]; decisionReason?: string | undefined }): Promise<Envelope<OrgRoleGrant>> {
227
+ return request(config, { method: "POST", path: orgPath("role-grants"), body: input });
218
228
  },
219
229
 
220
- /** GET /org-role-grants/<grantId> — single-read. */
230
+ /** GET /role-grants/<grantId> — single-read. */
221
231
  async get(grantId: string): Promise<Envelope<OrgRoleGrant>> {
222
- return request(config, { method: "GET", path: orgPath(`org-role-grants/${encodeURIComponent(grantId)}`) });
232
+ return request(config, { method: "GET", path: orgPath(`role-grants/${encodeURIComponent(grantId)}`) });
223
233
  },
224
234
 
225
- /** DELETE /org-role-grants/<grantId> — revoke a grant (soft delete; sets revokedAt). */
226
- async revoke(grantId: string): Promise<Envelope<OrgRoleGrant>> {
227
- return request(config, { method: "DELETE", path: orgPath(`org-role-grants/${encodeURIComponent(grantId)}`) });
235
+ /** DELETE /role-grants/<grantId> — revoke a grant (soft delete; sets revokedAt). */
236
+ async revoke(grantId: string, opts?: { decisionReason?: string | undefined }): Promise<Envelope<OrgRoleGrant>> {
237
+ return request(config, { method: "DELETE", path: orgPath(`role-grants/${encodeURIComponent(grantId)}`), ...(opts ? { body: opts } : {}) });
228
238
  },
229
239
  },
230
240
 
231
241
  idpProviders: {
232
- /** GET /idp-providers — list configured org operator IdP providers ask 7. */
242
+ /** GET /identity-providers — list configured identity providers. */
233
243
  async list(): Promise<Envelope<ReadonlyArray<OrgIdpProvider>>> {
234
- return request(config, { method: "GET", path: orgPath("idp-providers") });
244
+ return request(config, { method: "GET", path: orgPath("identity-providers") });
235
245
  },
236
246
 
237
- /** POST /idp-providers — create a new IdP integration. */
247
+ /** POST /identity-providers — create a new IdP integration. */
238
248
  async create(input: CreateOrgIdpInput): Promise<Envelope<OrgIdpProvider>> {
239
- return request(config, { method: "POST", path: orgPath("idp-providers"), body: input });
249
+ return request(config, { method: "POST", path: orgPath("identity-providers"), body: input });
240
250
  },
241
251
 
242
- /** GET /idp-providers/<idpId>. */
252
+ /** GET /identity-providers/<idpId>. */
243
253
  async get(idpId: string): Promise<Envelope<OrgIdpProvider>> {
244
- return request(config, { method: "GET", path: orgPath(`idp-providers/${encodeURIComponent(idpId)}`) });
254
+ return request(config, { method: "GET", path: orgPath(`identity-providers/${encodeURIComponent(idpId)}`) });
245
255
  },
246
256
 
247
- /** PATCH /idp-providers/<idpId> — update config or enable/disable. */
248
- async configure(idpId: string, patch: { name?: string; enabled?: boolean; config?: Record<string, unknown> }): Promise<Envelope<OrgIdpProvider>> {
249
- return request(config, { method: "PATCH", path: orgPath(`idp-providers/${encodeURIComponent(idpId)}`), body: patch });
257
+ /** PATCH /identity-providers/<idpId> — update settings or enable/disable. */
258
+ async configure(idpId: string, patch: UpdateOrgIdpInput): Promise<Envelope<OrgIdpProvider>> {
259
+ return request(config, { method: "PATCH", path: orgPath(`identity-providers/${encodeURIComponent(idpId)}`), body: patch });
250
260
  },
251
261
 
252
- /** DELETE /idp-providers/<idpId>. */
262
+ /** DELETE /identity-providers/<idpId>. */
253
263
  async delete(idpId: string): Promise<Envelope<{ deleted: boolean }>> {
254
- return request(config, { method: "DELETE", path: orgPath(`idp-providers/${encodeURIComponent(idpId)}`) });
264
+ return request(config, { method: "DELETE", path: orgPath(`identity-providers/${encodeURIComponent(idpId)}`) });
255
265
  },
256
266
  },
257
267
  };
@@ -260,21 +270,10 @@ function makeOrgGateNamespace(config: HttpClientConfig) {
260
270
  function makeSmtpNamespace(config: HttpClientConfig) {
261
271
  return {
262
272
  /**
263
- * GET /org/smtp — read current SMTP config.
264
- * Returns null when no org-level SMTP is configured (falls back to
265
- * platform SMTP 3-tier cascade).
266
- */
267
- async get(): Promise<Envelope<SmtpConfig | null>> {
268
- return request(config, { method: "GET", path: orgPath("org/smtp") });
269
- },
270
-
271
- /**
272
- * PATCH /orgs/<orgSlug> with `smtpConfig` body — set or update SMTP.
273
- * PATCH on the org resource accepts smtpConfig.
274
- * Passwords route through Kibo enclave — SDK accepts plaintext at
275
- * the SDK boundary; gateway stores the reference.
273
+ * PATCH /org with `smtpConfig` body — set or update SMTP. Answers the org record.
274
+ * The body carries `passwordRef` (a reference to the stored password), not the password.
276
275
  */
277
- async set(config_: Omit<SmtpConfig, "passwordRef"> & { password: string }): Promise<Envelope<SmtpConfig>> {
276
+ async set(config_: SmtpConfig): Promise<Envelope<Org>> {
278
277
  return request(config, { method: "PATCH", path: orgPath("org"), body: { smtpConfig: config_ } });
279
278
  },
280
279
 
@@ -287,8 +286,8 @@ function makeSmtpNamespace(config: HttpClientConfig) {
287
286
  return request(config, { method: "POST", path: orgPath("org/smtp/test-email"), body: input });
288
287
  },
289
288
 
290
- /** PATCH /orgs/<orgSlug> with `smtpConfig: null` — clear org-level SMTP. */
291
- async unset(): Promise<Envelope<{ cleared: boolean }>> {
289
+ /** PATCH /org with `smtpConfig: null` — clear org-level SMTP. Answers the org record. */
290
+ async unset(): Promise<Envelope<Org>> {
292
291
  return request(config, { method: "PATCH", path: orgPath("org"), body: { smtpConfig: null } });
293
292
  },
294
293
  };
@@ -297,21 +296,20 @@ function makeSmtpNamespace(config: HttpClientConfig) {
297
296
  function makeDefaultLlmNamespace(config: HttpClientConfig) {
298
297
  return {
299
298
  /**
300
- * GET /org/default-llm — read the org-default LLM model id
301
- *. Returns `null` when no explicit default is set (gateway
302
- * falls back to `mistral-7b`). ORG_ADMIN read; tier-gated.
299
+ * GET /default-llm — read the org-default LLM model id.
300
+ * Any accepted member of the org may read it.
303
301
  */
304
- async get(): Promise<Envelope<LLMModelId | null>> {
305
- return request(config, { method: "GET", path: orgPath("org/default-llm") });
302
+ async get(): Promise<Envelope<OrgDefaultLlm>> {
303
+ return request(config, { method: "GET", path: orgPath("default-llm") });
306
304
  },
307
305
 
308
306
  /**
309
- * PATCH /org/default-llm — set the org-default LLM.
310
- * ORG_ADMIN only. The model must be available to the org's tier per
307
+ * PATCH /default-llm — set the org-default LLM.
308
+ * ORG_ADMIN or ORG_OWNER only. The model must be available to the org's tier
311
309
  * or the gateway returns K9O_TIER_FEATURE_GATED.
312
310
  */
313
- async set(input: { modelId: LLMModelId }): Promise<Envelope<LLMModelId>> {
314
- return request(config, { method: "PATCH", path: orgPath("org/default-llm"), body: input });
311
+ async set(input: { modelId: LLMModelId }): Promise<Envelope<{ orgId: string; defaultModelId: string }>> {
312
+ return request(config, { method: "PATCH", path: orgPath("default-llm"), body: input });
315
313
  },
316
314
  };
317
315
  }
@@ -322,25 +320,25 @@ function makeDefaultLlmNamespace(config: HttpClientConfig) {
322
320
 
323
321
  export function makeOrgNamespace(config: HttpClientConfig) {
324
322
  return {
325
- /** Member lifecycle — list / invite / get / remove. */
323
+ /** Member lifecycle — list / invite / remove. */
326
324
  members: makeMembersNamespace(config),
327
- /** Group lifecycle — list / create / get / delete. */
325
+ /** Groups — list / get (read-only). */
328
326
  groups: makeGroupsNamespace(config),
329
- /** Org-plane audit log — query / tail. Distinct from project-plane `client.audit`. */
327
+ /** Org-plane audit log — query. Distinct from project-plane `client.audit`. */
330
328
  auditLog: makeOrgAuditLogNamespace(config),
331
329
  /** Gate (IAK) org-level surfaces — role-grants + IdP-providers. */
332
330
  gate: makeOrgGateNamespace(config),
333
- /** Org-level SMTP config — get / set / test / unset. */
331
+ /** Org-level SMTP config — set / test / unset. */
334
332
  smtp: makeSmtpNamespace(config),
335
333
  /** Org-default LLM — get / set (ORG_ADMIN). */
336
334
  defaultLLM: makeDefaultLlmNamespace(config),
337
335
 
338
336
  /**
339
- * GET /org — read the current org (slug + name + status + settings).
337
+ * GET /org — read the current org (slug, displayName, products, billing tier, your role).
340
338
  * Mirrors `client.orgs.current()` for symmetry; `client.org.get()` is
341
339
  * the singular-namespace convention for use within an org-bound client.
342
340
  */
343
- async get(): Promise<Envelope<{ id: string; slug: string; name: string; status: string; createdAt: string }>> {
341
+ async get(): Promise<Envelope<Org>> {
344
342
  return request(config, { method: "GET", path: orgPath("org") });
345
343
  },
346
344
  };
package/src/orgs.ts CHANGED
@@ -26,14 +26,27 @@ import { request, type HttpClientConfig } from "./http.ts";
26
26
  import { orgPath, validateSlug } from "./url-grammar.ts";
27
27
  import type { Envelope } from "./envelope.ts";
28
28
 
29
+ /**
30
+ * Org record, as the gateway returns it. `list` rows add `viaToken` and `consoleDomain`;
31
+ * `get`, `current` and `restore` add `customDomain` and `deletedAt`.
32
+ */
29
33
  export type Org = {
30
34
  /** `org_<base62>`. */
31
35
  id: string;
32
36
  slug: string;
33
- name: string;
34
- status: "active" | "suspended" | "soft-deleted";
37
+ displayName: string;
38
+ products: ReadonlyArray<string>;
39
+ billingTier: string;
40
+ signupProduct: string;
41
+ /** The caller's role in the org. */
42
+ role: string;
35
43
  createdAt: string;
36
- updatedAt: string | null;
44
+ customDomain?: string | null | undefined;
45
+ deletedAt?: string | null | undefined;
46
+ /** List rows only: `true` when the org is reached through the token rather than a membership. */
47
+ viaToken?: boolean | undefined;
48
+ /** List rows only: the org's Console host. */
49
+ consoleDomain?: string | undefined;
37
50
  };
38
51
 
39
52
  export function makeOrgsNamespace(config: HttpClientConfig) {
package/src/policies.ts CHANGED
@@ -13,9 +13,9 @@
13
13
  * - `delete(bundleId)` → DELETE /@<slug>/policies/<id>
14
14
  * - `publish(bundleId)` → POST /@<slug>/policies/<id>/publish
15
15
  *
16
- * Per: only Pathmaker-class roles author policy; only
17
- * Custodian-class roles publish. `validate` is read-only and any role
18
- * with policy-read can lint a candidate bundle.
16
+ * Only authoring roles write policy and only custodian roles publish.
17
+ * `validate` is read-only and any role with policy-read can lint a
18
+ * candidate document.
19
19
  */
20
20
 
21
21
  import { request, type HttpClientConfig } from "./http.ts";
@@ -25,29 +25,63 @@ import type { Envelope } from "./envelope.ts";
25
25
  export type PolicyBundle = {
26
26
  /** `pol_<base62>`. */
27
27
  id: string;
28
- name: string;
29
- version: number;
30
- status: "draft" | "published" | "deactivated";
31
- rules: Record<string, unknown>;
28
+ projectId: string;
29
+ status: "draft" | "published" | "superseded";
30
+ /** The policy document. */
31
+ policy: PolicyDocument;
32
+ /** The rules catalogue, when the bundle carries one. */
33
+ rules?: unknown;
34
+ createdBy: string;
32
35
  createdAt: string;
33
36
  publishedAt: string | null;
34
- createdBy: string;
37
+ supersededAt: string | null;
38
+ supersededBy: string | null;
39
+ source: "rest" | "migration" | "cli";
40
+ editorMode: "visual" | "json" | "unspecified";
41
+ supersededReason: string | null;
42
+ /** Id of a queued publish awaiting a second approver, else `null`. */
43
+ pendingChangeId: string | null;
44
+ };
45
+
46
+ /** Answer of `publish`: the publish is queued for approval (HTTP 202). */
47
+ export type PolicyPublishResult = {
48
+ pendingChangeId: string;
49
+ bundleId: string;
50
+ status: "pending_approval";
35
51
  };
36
52
 
53
+ /**
54
+ * The policy document: sections keyed by resource kind (`collections`,
55
+ * `storage`, `tools`, `functions`, `secrets`, `streams`, `analytics`);
56
+ * other keys are allowed.
57
+ */
58
+ export type PolicyDocument = Record<string, unknown>;
59
+
37
60
  export type CreatePolicyInput = {
38
- name: string;
39
- rules: Record<string, unknown>;
61
+ policy: PolicyDocument;
62
+ rules?: unknown;
63
+ editorMode?: "visual" | "json" | "unspecified" | undefined;
40
64
  };
41
65
 
42
66
  export type UpdatePolicyInput = {
43
- name?: string;
44
- rules?: Record<string, unknown>;
67
+ policy: PolicyDocument;
68
+ rules?: unknown;
69
+ editorMode?: "visual" | "json" | "unspecified" | undefined;
70
+ };
71
+
72
+ export type ValidatePolicyInput = {
73
+ policy: PolicyDocument;
74
+ rules?: unknown;
45
75
  };
46
76
 
77
+ /** Validation failures are errors, so a successful validate always answers `{ valid: true }`. */
47
78
  export type ValidatePolicyResult = {
48
- valid: boolean;
49
- errors: ReadonlyArray<{ rule: string; message: string }>;
50
- warnings: ReadonlyArray<{ rule: string; message: string }>;
79
+ valid: true;
80
+ };
81
+
82
+ /** Optional publish controls; property names are the gateway's. */
83
+ export type PublishPolicyOptions = {
84
+ acknowledgeSelfLockout?: boolean | undefined;
51
85
  };
52
86
 
53
87
  function requireProjectSlug(config: HttpClientConfig): string {
@@ -68,8 +102,8 @@ export function makePoliciesNamespace(config: HttpClientConfig) {
68
102
  });
69
103
  },
70
104
 
71
- /** List all policy bundles (draft + published + deactivated). */
72
- async list(opts?: { status?: PolicyBundle["status"] | undefined; limit?: number | undefined; cursor?: string | undefined }): Promise<Envelope<ReadonlyArray<PolicyBundle>>> {
105
+ /** List all policy bundles (draft + published + superseded). */
106
+ async list(opts?: { status?: PolicyBundle["status"] | undefined }): Promise<Envelope<ReadonlyArray<PolicyBundle>>> {
73
107
  const slug = requireProjectSlug(config);
74
108
  return request(config, {
75
109
  method: "GET",
@@ -88,8 +122,8 @@ export function makePoliciesNamespace(config: HttpClientConfig) {
88
122
  });
89
123
  },
90
124
 
91
- /** Lint a candidate rule set without persisting. */
92
- async validate(input: { rules: Record<string, unknown> }): Promise<Envelope<ValidatePolicyResult>> {
125
+ /** Lint a candidate policy document without persisting. */
126
+ async validate(input: ValidatePolicyInput): Promise<Envelope<ValidatePolicyResult>> {
93
127
  const slug = requireProjectSlug(config);
94
128
  return request(config, {
95
129
  method: "POST",
@@ -107,7 +141,7 @@ export function makePoliciesNamespace(config: HttpClientConfig) {
107
141
  });
108
142
  },
109
143
 
110
- /** Patch a draft bundle. Cannot patch published bundles (publish→republish flow). */
144
+ /** Replace a draft bundle's policy document. Cannot patch published bundles (publish→republish flow). */
111
145
  async update(bundleId: string, patch: UpdatePolicyInput): Promise<Envelope<PolicyBundle>> {
112
146
  const slug = requireProjectSlug(config);
113
147
  return request(config, {
@@ -117,8 +151,8 @@ export function makePoliciesNamespace(config: HttpClientConfig) {
117
151
  });
118
152
  },
119
153
 
120
- /** Delete a draft bundle. Cannot delete published bundles. */
121
- async delete(bundleId: string): Promise<Envelope<{ deleted: boolean }>> {
154
+ /** Delete a draft bundle. Cannot delete published bundles. The gateway answers 204 with no body. */
155
+ async delete(bundleId: string): Promise<Envelope<undefined>> {
122
156
  const slug = requireProjectSlug(config);
123
157
  return request(config, {
124
158
  method: "DELETE",
@@ -126,12 +160,17 @@ export function makePoliciesNamespace(config: HttpClientConfig) {
126
160
  });
127
161
  },
128
162
 
129
- /** Publish a draft bundle — transitions to `published` + becomes active for the project. */
130
- async publish(bundleId: string): Promise<Envelope<PolicyBundle>> {
163
+ /**
164
+ * Publish a draft bundle. The gateway answers 202 with `{ pendingChangeId, bundleId }`: the publish waits for
165
+ * approval, then the bundle becomes active. Pass `acknowledgeSelfLockout: true` to proceed when the bundle
166
+ * would remove your own read access to a collection.
167
+ */
168
+ async publish(bundleId: string, options?: PublishPolicyOptions): Promise<Envelope<PolicyPublishResult>> {
131
169
  const slug = requireProjectSlug(config);
132
170
  return request(config, {
133
171
  method: "POST",
134
172
  path: projectPath(slug, `policies/${encodeURIComponent(bundleId)}/publish`),
173
+ ...(options !== undefined ? { body: options } : {}),
135
174
  });
136
175
  },
137
176
  };