@plantops/contracts 0.1.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.
Files changed (60) hide show
  1. package/README.md +11 -0
  2. package/dist/audit.d.ts +149 -0
  3. package/dist/audit.d.ts.map +1 -0
  4. package/dist/audit.js +87 -0
  5. package/dist/bindings.d.ts +125 -0
  6. package/dist/bindings.d.ts.map +1 -0
  7. package/dist/bindings.js +40 -0
  8. package/dist/clients.d.ts +240 -0
  9. package/dist/clients.d.ts.map +1 -0
  10. package/dist/clients.js +49 -0
  11. package/dist/constants.d.ts +91 -0
  12. package/dist/constants.d.ts.map +1 -0
  13. package/dist/constants.js +92 -0
  14. package/dist/entitlements.d.ts +135 -0
  15. package/dist/entitlements.d.ts.map +1 -0
  16. package/dist/entitlements.js +43 -0
  17. package/dist/errors.d.ts +68 -0
  18. package/dist/errors.d.ts.map +1 -0
  19. package/dist/errors.js +83 -0
  20. package/dist/grants.d.ts +70 -0
  21. package/dist/grants.d.ts.map +1 -0
  22. package/dist/grants.js +13 -0
  23. package/dist/index.d.ts +34 -0
  24. package/dist/index.d.ts.map +1 -0
  25. package/dist/index.js +33 -0
  26. package/dist/jwt.d.ts +155 -0
  27. package/dist/jwt.d.ts.map +1 -0
  28. package/dist/jwt.js +49 -0
  29. package/dist/lib/contracts.d.ts +2 -0
  30. package/dist/lib/contracts.d.ts.map +1 -0
  31. package/dist/lib/contracts.js +3 -0
  32. package/dist/manifest.d.ts +136 -0
  33. package/dist/manifest.d.ts.map +1 -0
  34. package/dist/manifest.js +22 -0
  35. package/dist/nav.d.ts +53 -0
  36. package/dist/nav.d.ts.map +1 -0
  37. package/dist/nav.js +25 -0
  38. package/dist/pagination.d.ts +28 -0
  39. package/dist/pagination.d.ts.map +1 -0
  40. package/dist/pagination.js +27 -0
  41. package/dist/registry.d.ts +183 -0
  42. package/dist/registry.d.ts.map +1 -0
  43. package/dist/registry.js +24 -0
  44. package/dist/roles.d.ts +215 -0
  45. package/dist/roles.d.ts.map +1 -0
  46. package/dist/roles.js +45 -0
  47. package/dist/scopes.d.ts +220 -0
  48. package/dist/scopes.d.ts.map +1 -0
  49. package/dist/scopes.js +108 -0
  50. package/dist/service-accounts.d.ts +81 -0
  51. package/dist/service-accounts.d.ts.map +1 -0
  52. package/dist/service-accounts.js +35 -0
  53. package/dist/tsconfig.lib.tsbuildinfo +1 -0
  54. package/dist/type-assertions.d.ts +18 -0
  55. package/dist/type-assertions.d.ts.map +1 -0
  56. package/dist/type-assertions.js +8 -0
  57. package/dist/users.d.ts +364 -0
  58. package/dist/users.d.ts.map +1 -0
  59. package/dist/users.js +134 -0
  60. package/package.json +39 -0
package/README.md ADDED
@@ -0,0 +1,11 @@
1
+ # contracts
2
+
3
+ This library was generated with [Nx](https://nx.dev).
4
+
5
+ ## Building
6
+
7
+ Run `nx build contracts` to build the library.
8
+
9
+ ## Running unit tests
10
+
11
+ Run `nx test contracts` to execute the unit tests via [Jest](https://jestjs.io).
@@ -0,0 +1,149 @@
1
+ /**
2
+ * The audit read surface — `GET /iam/audit` and its CSV export
3
+ * (Doc 06 §12, Doc 10 §2, §7).
4
+ *
5
+ * The trail is written from a dozen places and read from exactly one, and this
6
+ * file describes the reading half. Everything about the writing half — the
7
+ * action catalog, the redaction boundary, the `SECURITY DEFINER` writer — is
8
+ * deliberately absent: a consumer of this contract cannot write an audit row and
9
+ * must not be given a type that suggests otherwise (Doc 10 §7: "audit is
10
+ * read-only through the API — there is no mutate/delete endpoint by design").
11
+ *
12
+ * ## Why `action` and `target_type` are plain strings here
13
+ *
14
+ * The IAM types both narrowly for its *writers* — `AUDIT_ACTIONS` and
15
+ * `AUDIT_TARGET_TYPES` in `apps/iam-api/src/audit/audit-actions.ts` — so a
16
+ * misspelling does not compile. A *reader* cannot be typed that way, and the
17
+ * reason is the whole point of an append-only table: rows outlive the catalog
18
+ * that wrote them. An action retired in a later version, or a `target_type`
19
+ * naming a table since renamed, is still in the trail and must still be
20
+ * readable; a union here would make the compiler refuse to describe history that
21
+ * the database is contractually obliged to keep (Doc 10 §1, §6).
22
+ *
23
+ * `actor_type` is the exception, and it is the exception for the opposite
24
+ * reason: it is a Postgres enum (migration 0001) rather than a convention, so
25
+ * the database itself guarantees the three values — see {@link AuditActorType}.
26
+ */
27
+ import type { PaginationQuery } from './pagination.js';
28
+ /**
29
+ * Who took the action (Doc 10 §2).
30
+ *
31
+ * Derived inside `iam.write_audit` from the session context rather than passed
32
+ * by any caller, which is what makes it non-forgeable (migration 0010): a
33
+ * platform context stamps `platform`, an authenticated user `user`, and anything
34
+ * else `service_account`.
35
+ *
36
+ * Spelled here rather than imported from `@plantops/db`, which contracts must
37
+ * not depend on — it has zero dependencies by design (Doc 08 §3). The Postgres
38
+ * enum is the same three values in the same order, and `libs/db`'s
39
+ * `entities.spec.ts` asserts the two spellings against each other so they cannot
40
+ * drift into an `actor_type` the API publishes and the column never produces.
41
+ * Same arrangement as `UserStatus` and `ClientStatus`.
42
+ */
43
+ export declare const AuditActorType: {
44
+ readonly USER: "user";
45
+ readonly SERVICE_ACCOUNT: "service_account";
46
+ readonly PLATFORM: "platform";
47
+ };
48
+ export type AuditActorType = (typeof AuditActorType)[keyof typeof AuditActorType];
49
+ export declare const AUDIT_ACTOR_TYPE_VALUES: readonly ["user", "service_account", "platform"];
50
+ /**
51
+ * One record of the trail, exactly as Doc 10 §2 shapes it (Doc 01 §4.8).
52
+ *
53
+ * Every nullable field is nullable for a stated reason, and none of them is an
54
+ * oversight:
55
+ *
56
+ * - **`client_id`** — null means a platform-level action, outside any tenant.
57
+ * Only a platform admin ever sees such a row (Doc 10 §7).
58
+ * - **`actor_id`** — null where there was no subject to name. `platform.bootstrap`
59
+ * runs before any subject exists (migration 0011), and a failed login names an
60
+ * account that may not have matched anything.
61
+ * - **`target_type` / `target_id`** — null where the event has no row to point
62
+ * at: a bulk upload reports counts, a denied request names a permission. The
63
+ * table's `audit_trail_target_is_typed` check means an id never arrives
64
+ * without a type, so the two are null together or the type alone is set.
65
+ *
66
+ * `payload` is whatever context the writer attached, after passing the redaction
67
+ * boundary of Doc 10 §8 — so a consumer may render it, but must not assume any
68
+ * particular key: the shape varies by action, and secrets are structurally
69
+ * absent rather than merely omitted.
70
+ */
71
+ export interface AuditRecordDTO {
72
+ id: string;
73
+ /** Null ⇒ a platform-level action, outside any tenant (Doc 10 §2). */
74
+ client_id: string | null;
75
+ actor_type: AuditActorType;
76
+ /** Null where the event had no subject to attribute. */
77
+ actor_id: string | null;
78
+ /** A dotted verb from the Doc 10 §4 catalog — see the header on typing. */
79
+ action: string;
80
+ /** The table the target lives in, or null where the event names no row. */
81
+ target_type: string | null;
82
+ target_id: string | null;
83
+ /** Redacted context (Doc 10 §8). Shape varies by action. */
84
+ payload: Record<string, unknown>;
85
+ /** ISO-8601. */
86
+ created_at: string;
87
+ }
88
+ /**
89
+ * The filters of Doc 06 §12, composable in any combination (Doc 10 §1.4:
90
+ * "filterable by actor, action, target, client, and time").
91
+ *
92
+ * Every one of them **narrows** and none of them widens. That is the property
93
+ * that makes the same query object safe for both tiers: visibility is decided by
94
+ * the `audit_trail_read` RLS policy from the caller's own context (migration
95
+ * 0010), so a client admin passing `client_id` of another tenant gets an empty
96
+ * page rather than a refusal — the same answer they would get for a client that
97
+ * does not exist, which is what Doc 06 §2 requires of anything that could
98
+ * otherwise become a cross-tenant existence oracle.
99
+ *
100
+ * `from` and `to` are ISO-8601 instants compared against `created_at`, `from`
101
+ * inclusive and `to` exclusive — the half-open convention that lets a caller
102
+ * page a month by asking for `[2026-08-01, 2026-09-01)` without owning the
103
+ * boundary problem.
104
+ */
105
+ export interface AuditQuery extends PaginationQuery {
106
+ /** The subject who acted — `actor_id`. */
107
+ actor_id?: string;
108
+ actor_type?: AuditActorType;
109
+ /** An exact action from the Doc 10 §4 catalog. */
110
+ action?: string;
111
+ target_type?: string;
112
+ target_id?: string;
113
+ /** The tenant whose rows to show; only ever narrows what RLS already allows. */
114
+ client_id?: string;
115
+ /** Inclusive lower bound on `created_at`, ISO-8601. */
116
+ from?: string;
117
+ /** Exclusive upper bound on `created_at`, ISO-8601. */
118
+ to?: string;
119
+ }
120
+ /** {@link AuditQuery} without the page — an export is of the whole filter. */
121
+ export type AuditExportQuery = Omit<AuditQuery, 'page' | 'limit'>;
122
+ /**
123
+ * The most rows one CSV export may carry.
124
+ *
125
+ * A ceiling rather than a page, because an export is a document: a compliance
126
+ * reader who asked for a quarter and silently received its first ten thousand
127
+ * events has a file that *looks* complete and is not, which is a worse failure
128
+ * than no file at all. So a filter matching more than this is refused with the
129
+ * count and a suggestion to narrow the range (Doc 06 §2's `VALIDATION_FAILED`),
130
+ * and every export that succeeds is the complete answer to what was asked.
131
+ *
132
+ * Ten thousand rows of this shape is a few megabytes — comfortably inside what a
133
+ * spreadsheet opens and what a response can be assembled in.
134
+ */
135
+ export declare const AUDIT_EXPORT_MAX_ROWS = 10000;
136
+ /**
137
+ * The CSV column order, which is part of the contract.
138
+ *
139
+ * An export is consumed by spreadsheets and by scripts, and both break when
140
+ * columns move. Publishing the order here rather than leaving it inside the
141
+ * server means a consumer can assert on it, and means the header row and the
142
+ * value row cannot disagree — `audit-csv.ts` builds both from this list.
143
+ */
144
+ export declare const AUDIT_EXPORT_COLUMNS: readonly ["id", "created_at", "client_id", "actor_type", "actor_id", "action", "target_type", "target_id", "payload"];
145
+ export type AuditExportColumn = (typeof AUDIT_EXPORT_COLUMNS)[number];
146
+ /** `Content-Type` of the export, and what the browser saves it as. */
147
+ export declare const AUDIT_EXPORT_CONTENT_TYPE = "text/csv; charset=utf-8";
148
+ export declare const AUDIT_EXPORT_FILENAME = "audit-export.csv";
149
+ //# sourceMappingURL=audit.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"audit.d.ts","sourceRoot":"","sources":["../src/audit.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAEH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AAEvD;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,cAAc;;;;CAIjB,CAAC;AACX,MAAM,MAAM,cAAc,GAAG,CAAC,OAAO,cAAc,CAAC,CAAC,MAAM,OAAO,cAAc,CAAC,CAAC;AAElF,eAAO,MAAM,uBAAuB,kDAIU,CAAC;AAE/C;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,MAAM,WAAW,cAAc;IAC7B,EAAE,EAAE,MAAM,CAAC;IACX,sEAAsE;IACtE,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,UAAU,EAAE,cAAc,CAAC;IAC3B,wDAAwD;IACxD,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,2EAA2E;IAC3E,MAAM,EAAE,MAAM,CAAC;IACf,2EAA2E;IAC3E,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,4DAA4D;IAC5D,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACjC,gBAAgB;IAChB,UAAU,EAAE,MAAM,CAAC;CACpB;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,WAAW,UAAW,SAAQ,eAAe;IACjD,0CAA0C;IAC1C,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,UAAU,CAAC,EAAE,cAAc,CAAC;IAC5B,kDAAkD;IAClD,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,gFAAgF;IAChF,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,uDAAuD;IACvD,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,uDAAuD;IACvD,EAAE,CAAC,EAAE,MAAM,CAAC;CACb;AAED,8EAA8E;AAC9E,MAAM,MAAM,gBAAgB,GAAG,IAAI,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,CAAC;AAElE;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,qBAAqB,QAAS,CAAC;AAE5C;;;;;;;GAOG;AACH,eAAO,MAAM,oBAAoB,uHAUqB,CAAC;AAEvD,MAAM,MAAM,iBAAiB,GAAG,CAAC,OAAO,oBAAoB,CAAC,CAAC,MAAM,CAAC,CAAC;AAEtE,sEAAsE;AACtE,eAAO,MAAM,yBAAyB,4BAA4B,CAAC;AACnE,eAAO,MAAM,qBAAqB,qBAAqB,CAAC"}
package/dist/audit.js ADDED
@@ -0,0 +1,87 @@
1
+ /**
2
+ * The audit read surface — `GET /iam/audit` and its CSV export
3
+ * (Doc 06 §12, Doc 10 §2, §7).
4
+ *
5
+ * The trail is written from a dozen places and read from exactly one, and this
6
+ * file describes the reading half. Everything about the writing half — the
7
+ * action catalog, the redaction boundary, the `SECURITY DEFINER` writer — is
8
+ * deliberately absent: a consumer of this contract cannot write an audit row and
9
+ * must not be given a type that suggests otherwise (Doc 10 §7: "audit is
10
+ * read-only through the API — there is no mutate/delete endpoint by design").
11
+ *
12
+ * ## Why `action` and `target_type` are plain strings here
13
+ *
14
+ * The IAM types both narrowly for its *writers* — `AUDIT_ACTIONS` and
15
+ * `AUDIT_TARGET_TYPES` in `apps/iam-api/src/audit/audit-actions.ts` — so a
16
+ * misspelling does not compile. A *reader* cannot be typed that way, and the
17
+ * reason is the whole point of an append-only table: rows outlive the catalog
18
+ * that wrote them. An action retired in a later version, or a `target_type`
19
+ * naming a table since renamed, is still in the trail and must still be
20
+ * readable; a union here would make the compiler refuse to describe history that
21
+ * the database is contractually obliged to keep (Doc 10 §1, §6).
22
+ *
23
+ * `actor_type` is the exception, and it is the exception for the opposite
24
+ * reason: it is a Postgres enum (migration 0001) rather than a convention, so
25
+ * the database itself guarantees the three values — see {@link AuditActorType}.
26
+ */
27
+ /**
28
+ * Who took the action (Doc 10 §2).
29
+ *
30
+ * Derived inside `iam.write_audit` from the session context rather than passed
31
+ * by any caller, which is what makes it non-forgeable (migration 0010): a
32
+ * platform context stamps `platform`, an authenticated user `user`, and anything
33
+ * else `service_account`.
34
+ *
35
+ * Spelled here rather than imported from `@plantops/db`, which contracts must
36
+ * not depend on — it has zero dependencies by design (Doc 08 §3). The Postgres
37
+ * enum is the same three values in the same order, and `libs/db`'s
38
+ * `entities.spec.ts` asserts the two spellings against each other so they cannot
39
+ * drift into an `actor_type` the API publishes and the column never produces.
40
+ * Same arrangement as `UserStatus` and `ClientStatus`.
41
+ */
42
+ export const AuditActorType = {
43
+ USER: 'user',
44
+ SERVICE_ACCOUNT: 'service_account',
45
+ PLATFORM: 'platform',
46
+ };
47
+ export const AUDIT_ACTOR_TYPE_VALUES = [
48
+ AuditActorType.USER,
49
+ AuditActorType.SERVICE_ACCOUNT,
50
+ AuditActorType.PLATFORM,
51
+ ];
52
+ /**
53
+ * The most rows one CSV export may carry.
54
+ *
55
+ * A ceiling rather than a page, because an export is a document: a compliance
56
+ * reader who asked for a quarter and silently received its first ten thousand
57
+ * events has a file that *looks* complete and is not, which is a worse failure
58
+ * than no file at all. So a filter matching more than this is refused with the
59
+ * count and a suggestion to narrow the range (Doc 06 §2's `VALIDATION_FAILED`),
60
+ * and every export that succeeds is the complete answer to what was asked.
61
+ *
62
+ * Ten thousand rows of this shape is a few megabytes — comfortably inside what a
63
+ * spreadsheet opens and what a response can be assembled in.
64
+ */
65
+ export const AUDIT_EXPORT_MAX_ROWS = 10_000;
66
+ /**
67
+ * The CSV column order, which is part of the contract.
68
+ *
69
+ * An export is consumed by spreadsheets and by scripts, and both break when
70
+ * columns move. Publishing the order here rather than leaving it inside the
71
+ * server means a consumer can assert on it, and means the header row and the
72
+ * value row cannot disagree — `audit-csv.ts` builds both from this list.
73
+ */
74
+ export const AUDIT_EXPORT_COLUMNS = [
75
+ 'id',
76
+ 'created_at',
77
+ 'client_id',
78
+ 'actor_type',
79
+ 'actor_id',
80
+ 'action',
81
+ 'target_type',
82
+ 'target_id',
83
+ 'payload',
84
+ ];
85
+ /** `Content-Type` of the export, and what the browser saves it as. */
86
+ export const AUDIT_EXPORT_CONTENT_TYPE = 'text/csv; charset=utf-8';
87
+ export const AUDIT_EXPORT_FILENAME = 'audit-export.csv';
@@ -0,0 +1,125 @@
1
+ /**
2
+ * Role-binding contract — WHO × WHAT × WHERE in one row (Doc 01 §4.5,
3
+ * Doc 06 §9, Doc 09 §3.4).
4
+ *
5
+ * The other three client-tier contracts each describe one dimension: a
6
+ * {@link UserDTO} or a {@link ServiceAccountDTO} is a WHO, a {@link RoleDTO} is
7
+ * a bundle of WHATs, a {@link ScopeNodeDTO} is a WHERE. None of them grants
8
+ * anything. A binding is the only shape in the system that does, and everything
9
+ * below follows from that.
10
+ *
11
+ * ## One subject, expressed as one pair
12
+ *
13
+ * The table carries two nullable subject columns with a check constraint making
14
+ * exactly one of them set (Doc 01 §4.5, migration 0004). That is the right shape
15
+ * for a schema — a subject is genuinely a user *or* a service account, with a
16
+ * foreign key each — and the wrong shape for a response: a consumer that had to
17
+ * test which of two fields was populated would be re-deriving, in every client,
18
+ * what the constraint already guarantees. So {@link RoleBindingDTO} publishes
19
+ * {@link RoleBindingDTO.subject_type} and {@link RoleBindingDTO.subject_id},
20
+ * which is the same collapse the duplicate-prevention index makes with its
21
+ * `coalesce(user_id, service_account_id)`.
22
+ *
23
+ * {@link CreateRoleBindingRequest} goes the other way and keeps the two columns
24
+ * apart, because a request is where the XOR is *decided* rather than reported —
25
+ * a `subject_type` discriminator there would let a caller name a user id and
26
+ * label it a service account, and the resulting 409 would be about the wrong
27
+ * thing.
28
+ *
29
+ * ## Expiry does not remove a binding from this surface
30
+ *
31
+ * `expires_at` is enforced at resolve time (Doc 04 §4.1) and nowhere else: a
32
+ * lapsed grant stops granting and stays a row. So expired bindings are listed
33
+ * and flagged rather than filtered, for the reason {@link UserBindingDTO.expired}
34
+ * gives — "why did this stop working last Friday" is a question only the row can
35
+ * answer — and because Doc 01 §4.5 makes time passing a thing that fires no
36
+ * event at all.
37
+ *
38
+ * Field naming is snake_case, matching every other published shape here.
39
+ */
40
+ import type { SubjectType } from './jwt.js';
41
+ import type { PaginationQuery } from './pagination.js';
42
+ /**
43
+ * One grant (Doc 01 §4.5).
44
+ *
45
+ * The names beside every id — `role_name`, `scope_node_name`, `subject_name` —
46
+ * are joined in rather than left to the caller, for the reason
47
+ * {@link UserBindingDTO} gives: a binding rendered as four uuids is not a
48
+ * rendering of anything, and Doc 09 §3.4's list is the screen an operator
49
+ * reviews access on.
50
+ */
51
+ export interface RoleBindingDTO {
52
+ /** The row `DELETE /iam/role-bindings/:id` takes. */
53
+ id: string;
54
+ client_id: string;
55
+ /** Which of the two subject columns is set — see the header. */
56
+ subject_type: SubjectType;
57
+ /** The `user` or `service_account` id, whichever {@link subject_type} names. */
58
+ subject_id: string;
59
+ /** `user.full_name` or `service_account.name`. */
60
+ subject_name: string;
61
+ /** The user's address; `null` for a service account, which has none. */
62
+ subject_email: string | null;
63
+ role_id: string;
64
+ role_name: string;
65
+ scope_node_id: string;
66
+ scope_node_name: string;
67
+ /** The materialized `ltree` path — `n_<hex>` labels, never display names (Doc 01 §3.5). */
68
+ scope_node_path: string;
69
+ /** ISO-8601, or `null` for a grant that does not expire. */
70
+ expires_at: string | null;
71
+ /** Whether {@link expires_at} has passed — see the header. */
72
+ expired: boolean;
73
+ created_at: string;
74
+ }
75
+ /**
76
+ * `POST /iam/role-bindings` body (Doc 06 §9, Doc 09 §3.4).
77
+ *
78
+ * Doc 09 §3.4's single action: pick a subject, pick a role, pick a scope node,
79
+ * optionally set an expiry.
80
+ *
81
+ * `client_id` is absent, like everywhere else on the client tier: the tenant is
82
+ * the token's `cid`, and every one of the three ids is checked against it before
83
+ * a row is written (Doc 02 §6). Naming a role, node or subject from another
84
+ * client is a `409`, indistinguishable from naming one that does not exist —
85
+ * a binding request must not become a way to enumerate another tenant.
86
+ *
87
+ * Exactly one of {@link user_id} / {@link service_account_id}. Neither and both
88
+ * are `400`s: the body is malformed in a way no row could settle, so it is
89
+ * refused by the schema rather than by the check constraint.
90
+ *
91
+ * `expires_at` is ISO-8601 and must be in the future. A grant that lapsed before
92
+ * it was written grants nothing from the instant it exists, which is never what
93
+ * the operator meant.
94
+ */
95
+ export interface CreateRoleBindingRequest {
96
+ user_id?: string;
97
+ service_account_id?: string;
98
+ role_id: string;
99
+ scope_node_id: string;
100
+ /** ISO-8601, in the future. Omit for a grant that does not expire. */
101
+ expires_at?: string;
102
+ }
103
+ /**
104
+ * `GET /iam/role-bindings` query (Doc 06 §9, Doc 09 §3.4).
105
+ *
106
+ * Doc 06 §9's "filter by user, role, scope", with the subject split into its two
107
+ * columns for the reason {@link CreateRoleBindingRequest} splits them.
108
+ *
109
+ * `scope_node_id` matches the node the binding is **anchored to**, not the
110
+ * subtree it covers. The two are different questions — "what was granted here"
111
+ * against "who can act here" — and only the first is a list of bindings; the
112
+ * second is `POST /iam/permissions/check` (Doc 06 §11), which answers it for a
113
+ * subject rather than by enumerating rows.
114
+ *
115
+ * Every filter is combined with `and`, so an unfiltered call is the tenant's
116
+ * whole grant table and a fully filtered one is the duplicate check an operator
117
+ * can run before binding.
118
+ */
119
+ export interface RoleBindingsQuery extends PaginationQuery {
120
+ user_id?: string;
121
+ service_account_id?: string;
122
+ role_id?: string;
123
+ scope_node_id?: string;
124
+ }
125
+ //# sourceMappingURL=bindings.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bindings.d.ts","sourceRoot":"","sources":["../src/bindings.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsCG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,UAAU,CAAC;AAC5C,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AAEvD;;;;;;;;GAQG;AACH,MAAM,WAAW,cAAc;IAC7B,qDAAqD;IACrD,EAAE,EAAE,MAAM,CAAC;IACX,SAAS,EAAE,MAAM,CAAC;IAClB,gEAAgE;IAChE,YAAY,EAAE,WAAW,CAAC;IAC1B,gFAAgF;IAChF,UAAU,EAAE,MAAM,CAAC;IACnB,kDAAkD;IAClD,YAAY,EAAE,MAAM,CAAC;IACrB,wEAAwE;IACxE,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,aAAa,EAAE,MAAM,CAAC;IACtB,eAAe,EAAE,MAAM,CAAC;IACxB,2FAA2F;IAC3F,eAAe,EAAE,MAAM,CAAC;IACxB,4DAA4D;IAC5D,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,8DAA8D;IAC9D,OAAO,EAAE,OAAO,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;CACpB;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,MAAM,WAAW,wBAAwB;IACvC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,OAAO,EAAE,MAAM,CAAC;IAChB,aAAa,EAAE,MAAM,CAAC;IACtB,sEAAsE;IACtE,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED;;;;;;;;;;;;;;;GAeG;AACH,MAAM,WAAW,iBAAkB,SAAQ,eAAe;IACxD,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB"}
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Role-binding contract — WHO × WHAT × WHERE in one row (Doc 01 §4.5,
3
+ * Doc 06 §9, Doc 09 §3.4).
4
+ *
5
+ * The other three client-tier contracts each describe one dimension: a
6
+ * {@link UserDTO} or a {@link ServiceAccountDTO} is a WHO, a {@link RoleDTO} is
7
+ * a bundle of WHATs, a {@link ScopeNodeDTO} is a WHERE. None of them grants
8
+ * anything. A binding is the only shape in the system that does, and everything
9
+ * below follows from that.
10
+ *
11
+ * ## One subject, expressed as one pair
12
+ *
13
+ * The table carries two nullable subject columns with a check constraint making
14
+ * exactly one of them set (Doc 01 §4.5, migration 0004). That is the right shape
15
+ * for a schema — a subject is genuinely a user *or* a service account, with a
16
+ * foreign key each — and the wrong shape for a response: a consumer that had to
17
+ * test which of two fields was populated would be re-deriving, in every client,
18
+ * what the constraint already guarantees. So {@link RoleBindingDTO} publishes
19
+ * {@link RoleBindingDTO.subject_type} and {@link RoleBindingDTO.subject_id},
20
+ * which is the same collapse the duplicate-prevention index makes with its
21
+ * `coalesce(user_id, service_account_id)`.
22
+ *
23
+ * {@link CreateRoleBindingRequest} goes the other way and keeps the two columns
24
+ * apart, because a request is where the XOR is *decided* rather than reported —
25
+ * a `subject_type` discriminator there would let a caller name a user id and
26
+ * label it a service account, and the resulting 409 would be about the wrong
27
+ * thing.
28
+ *
29
+ * ## Expiry does not remove a binding from this surface
30
+ *
31
+ * `expires_at` is enforced at resolve time (Doc 04 §4.1) and nowhere else: a
32
+ * lapsed grant stops granting and stays a row. So expired bindings are listed
33
+ * and flagged rather than filtered, for the reason {@link UserBindingDTO.expired}
34
+ * gives — "why did this stop working last Friday" is a question only the row can
35
+ * answer — and because Doc 01 §4.5 makes time passing a thing that fires no
36
+ * event at all.
37
+ *
38
+ * Field naming is snake_case, matching every other published shape here.
39
+ */
40
+ export {};
@@ -0,0 +1,240 @@
1
+ /**
2
+ * Client contract — the tenant-provisioning surface (Doc 06 §5, Doc 02 §3).
3
+ *
4
+ * Three things live here, and they are three because provisioning a tenant is
5
+ * three separable decisions: **who the tenant is** ({@link ClientDTO}), **what
6
+ * they are allowed to run** ({@link ClientApplicationDTO}), and **who
7
+ * administers them on day one** ({@link ClientAdminDTO}). Doc 02 §3 lists them
8
+ * in that order and the API follows it, because each step is only meaningful
9
+ * once the one before it has happened.
10
+ *
11
+ * ## These are tenant rows described by a platform API
12
+ *
13
+ * Unlike {@link ApplicationDTO} and friends, everything here carries a
14
+ * `client_id` and lives behind RLS (Doc 07 §6). The surface is nonetheless
15
+ * platform-tier: a tenant does not create itself, and the endpoints are gated on
16
+ * `iam.platform.*`. That split is why the DTOs name the client explicitly rather
17
+ * than leaving it implied by the caller's token, the way the client-admin
18
+ * surfaces of Doc 06 §6–9 do.
19
+ *
20
+ * Field naming is snake_case, matching every other published shape in this
21
+ * package.
22
+ */
23
+ /**
24
+ * A tenant's lifecycle states (Doc 01 §3.4).
25
+ *
26
+ * Two, and neither is `deleted`: a tenant is suspended, never removed. Doc 06 §5
27
+ * makes suspension the off switch — a suspended client's users cannot log in,
28
+ * which migration 0012's `auth_begin_session` enforces by re-checking
29
+ * `client.status` at the moment a session is created — while every scope node,
30
+ * role, binding and audit row it owns stays exactly where it was. Deleting a
31
+ * client would take an organisation's entire access history with it, and
32
+ * `on delete restrict` on every child table (migration 0003) makes sure nobody
33
+ * does it by accident.
34
+ *
35
+ * Spelled here rather than imported from `@plantops/db`, which contracts must
36
+ * not depend on — it has zero dependencies by design (Doc 08 §3). The Postgres
37
+ * enum is the same two values, and `libs/db`'s `entities.spec.ts` asserts the
38
+ * two spellings against each other so they cannot drift into a status the API
39
+ * accepts and the column rejects. Same arrangement as
40
+ * {@link ServiceAccountStatus}.
41
+ */
42
+ export declare const ClientStatus: {
43
+ readonly ACTIVE: "active";
44
+ readonly SUSPENDED: "suspended";
45
+ };
46
+ export type ClientStatus = (typeof ClientStatus)[keyof typeof ClientStatus];
47
+ export declare const CLIENT_STATUS_VALUES: readonly ["active", "suspended"];
48
+ /**
49
+ * One tenant (Doc 01 §3.4).
50
+ *
51
+ * `slug` is half of the login credential — `POST /auth/login` takes
52
+ * `{ email, password, client_slug }` (Doc 03 §3) — which is why it is returned
53
+ * on every read and why nothing can change it.
54
+ */
55
+ export interface ClientDTO {
56
+ id: string;
57
+ name: string;
58
+ /** Lowercase kebab-case, and the half of the login credential a user types. */
59
+ slug: string;
60
+ status: ClientStatus;
61
+ /** Free-form per-tenant settings (Doc 01 §3.4). */
62
+ config: Record<string, unknown>;
63
+ /**
64
+ * How many applications this client currently has **enabled**.
65
+ *
66
+ * Included because Doc 09 §2.2's client list shows exactly this column, and a
67
+ * list view that had to issue one extra request per row to render it would be
68
+ * a list view nobody keeps. Disabled `client_application` rows are not counted
69
+ * — they are preserved, not active (Doc 02 §7).
70
+ */
71
+ enabled_application_count: number;
72
+ /**
73
+ * Entitlements (Doc 11 §10, migration 0020). `null` is unlimited — the
74
+ * reading every pre-licence deployment relies on — and `expires_at` is the
75
+ * subscription term whose passing blocks administrative writes while leaving
76
+ * authentication and resolution working.
77
+ */
78
+ expires_at: string | null;
79
+ max_users: number | null;
80
+ max_sites: number | null;
81
+ /** ISO-8601. */
82
+ created_at: string;
83
+ updated_at: string;
84
+ }
85
+ /**
86
+ * One application enabled for one client (Doc 01 §4.1).
87
+ *
88
+ * The application's `key` and `name` ride along with the ids for the reason
89
+ * {@link NavNodeCatalogDTO} carries `requires`: the toggle list of Doc 09 §2.2
90
+ * is unreadable as a column of uuids, and a consumer that had to join the
91
+ * catalog itself would be keeping a second copy of it.
92
+ *
93
+ * `enabled = false` is the tenant-level off switch of Doc 02 §7. The row stays,
94
+ * with its config, and every role mapping that referenced the app's permissions
95
+ * stays intact but inert — which is what makes re-enabling a toggle rather than
96
+ * a re-setup.
97
+ */
98
+ export interface ClientApplicationDTO {
99
+ client_id: string;
100
+ application_id: string;
101
+ /** The application's machine key, e.g. `gatepass`. */
102
+ application_key: string;
103
+ application_name: string;
104
+ enabled: boolean;
105
+ /** Per-client settings for this application (Doc 01 §4.1). */
106
+ config: Record<string, unknown>;
107
+ /**
108
+ * The per-module bundle entitlements (Doc 11 §10, migration 0020). `null`
109
+ * inherits the tenant-wide value from the client row.
110
+ */
111
+ expires_at: string | null;
112
+ max_users: number | null;
113
+ max_sites: number | null;
114
+ /** ISO-8601. */
115
+ created_at: string;
116
+ updated_at: string;
117
+ }
118
+ /**
119
+ * What `POST /iam/clients/:id/admins` created (Doc 06 §5, Doc 09 §2.2).
120
+ *
121
+ * Flat, and deliberately not a `UserDTO`: this describes the **bootstrap**, not
122
+ * a user resource. One call creates four rows that are only useful together —
123
+ * the user, the client's root scope node, the system client-admin role, and the
124
+ * binding that ties them — and the caller's next question is "can this person
125
+ * log in and see their whole organisation", which the four ids answer. The user
126
+ * management surface, and the `UserDTO` it publishes, is Doc 06 §8 / Session 18.
127
+ *
128
+ * No credential appears here. The password is chosen by the caller and is never
129
+ * echoed, stored in the clear, or written to the audit payload (Doc 10 §8).
130
+ */
131
+ export interface ClientAdminDTO {
132
+ client_id: string;
133
+ user_id: string;
134
+ email: string;
135
+ full_name: string;
136
+ /** The system role the user was bound to — `is_system`, per Doc 06 §5. */
137
+ role_id: string;
138
+ role_name: string;
139
+ /** The client's root scope node: everything the admin's grants cover. */
140
+ scope_node_id: string;
141
+ scope_node_name: string;
142
+ /** The root's ltree path — one `n_<hex>` label (Doc 01 §3.5). */
143
+ scope_node_path: string;
144
+ role_binding_id: string;
145
+ /** ISO-8601. */
146
+ created_at: string;
147
+ }
148
+ /** `POST /iam/clients` body (Doc 06 §5). */
149
+ export interface CreateClientRequest {
150
+ name: string;
151
+ slug: string;
152
+ config?: Record<string, unknown>;
153
+ }
154
+ /**
155
+ * `PATCH /iam/clients/:id` body (Doc 06 §5) — update, or suspend / reactivate.
156
+ *
157
+ * `slug` is absent on purpose, for a sharper version of the reason
158
+ * {@link UpdateApplicationRequest} omits `key`: the slug is not merely a natural
159
+ * key, it is something every one of the tenant's users types to log in
160
+ * (Doc 03 §3). Renaming it would lock out an entire organisation at once, and
161
+ * nothing in the row would look wrong afterwards.
162
+ */
163
+ export interface UpdateClientRequest {
164
+ name?: string;
165
+ status?: ClientStatus;
166
+ config?: Record<string, unknown>;
167
+ }
168
+ /**
169
+ * One entry of the `POST /iam/clients/:id/applications` body.
170
+ *
171
+ * The application is named by **either** `application_id` or `application_key`,
172
+ * never both — the same rule, for the same reason, as
173
+ * {@link CreateNavNodeInput}'s two ways of naming a parent. The key is what an
174
+ * operator recognises and what a manifest is written against; the uuid is what a
175
+ * UI already holds after listing the catalog.
176
+ */
177
+ export interface EnableApplicationInput {
178
+ application_id?: string;
179
+ application_key?: string;
180
+ config?: Record<string, unknown>;
181
+ }
182
+ /** `POST /iam/clients/:id/applications` body — bulk (Doc 02 §3 step 2). */
183
+ export interface EnableApplicationsRequest {
184
+ applications: EnableApplicationInput[];
185
+ }
186
+ /** `PATCH /iam/clients/:id/applications/:appId` body (Doc 06 §5). */
187
+ export interface UpdateClientApplicationRequest {
188
+ enabled?: boolean;
189
+ config?: Record<string, unknown>;
190
+ }
191
+ /**
192
+ * `PATCH /iam/clients/:id/scopes/:scopeId/billable-site` — the site meter
193
+ * (Doc 11 §10.1).
194
+ *
195
+ * Platform-tier, and permanently so. The meter deliberately does not count
196
+ * `scope_node.kind`, because kind is tenant-supplied text (ADR 0002) and a
197
+ * quantity the customer can rename away is not a quantity anyone can invoice.
198
+ */
199
+ export interface BillableSiteRequest {
200
+ billable_site: boolean;
201
+ }
202
+ /**
203
+ * `PATCH /iam/clients/:id/billable-sites` — the same meter, many nodes.
204
+ *
205
+ * One call for provisioning a tenant with dozens of sites. **All-or-nothing**:
206
+ * a batch that would breach `max_sites` is refused entire, naming the ceiling
207
+ * and what the batch would reach, because a half-applied billing change is a
208
+ * state nobody can reconcile later.
209
+ *
210
+ * Ids that are not this tenant's are absent from the response rather than an
211
+ * error — the same non-answer the single-node route gives, so that neither
212
+ * becomes an existence oracle over another tenant's tree.
213
+ */
214
+ export interface BillableSitesRequest {
215
+ scope_node_ids: string[];
216
+ billable_site: boolean;
217
+ }
218
+ /**
219
+ * `POST /iam/clients/:id/admins` body (Doc 06 §5).
220
+ *
221
+ * `password` is chosen rather than generated, unlike a service-account secret.
222
+ * The distinction is what the credential is: a machine secret is never typed by
223
+ * anyone, so minting it removes a decision nobody benefits from making, whereas
224
+ * this one belongs to a person who is about to type it and change it. Doc 03 §7
225
+ * already governs its strength, and the tokenized reset of Doc 06 §3 is
226
+ * available from the moment the account exists.
227
+ *
228
+ * `scope_name` names the root of the tenant's org tree — the node every grant in
229
+ * this call covers. It defaults to the client's own name, which is what an
230
+ * operator means by it nine times out of ten; the tree beneath it is the
231
+ * client admin's to build (Doc 06 §6).
232
+ */
233
+ export interface CreateClientAdminRequest {
234
+ email: string;
235
+ full_name: string;
236
+ password: string;
237
+ phone?: string;
238
+ scope_name?: string;
239
+ }
240
+ //# sourceMappingURL=clients.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"clients.d.ts","sourceRoot":"","sources":["../src/clients.ts"],"names":[],"mappings":"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"}