@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.
- package/README.md +11 -0
- package/dist/audit.d.ts +149 -0
- package/dist/audit.d.ts.map +1 -0
- package/dist/audit.js +87 -0
- package/dist/bindings.d.ts +125 -0
- package/dist/bindings.d.ts.map +1 -0
- package/dist/bindings.js +40 -0
- package/dist/clients.d.ts +240 -0
- package/dist/clients.d.ts.map +1 -0
- package/dist/clients.js +49 -0
- package/dist/constants.d.ts +91 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/constants.js +92 -0
- package/dist/entitlements.d.ts +135 -0
- package/dist/entitlements.d.ts.map +1 -0
- package/dist/entitlements.js +43 -0
- package/dist/errors.d.ts +68 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +83 -0
- package/dist/grants.d.ts +70 -0
- package/dist/grants.d.ts.map +1 -0
- package/dist/grants.js +13 -0
- package/dist/index.d.ts +34 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +33 -0
- package/dist/jwt.d.ts +155 -0
- package/dist/jwt.d.ts.map +1 -0
- package/dist/jwt.js +49 -0
- package/dist/lib/contracts.d.ts +2 -0
- package/dist/lib/contracts.d.ts.map +1 -0
- package/dist/lib/contracts.js +3 -0
- package/dist/manifest.d.ts +136 -0
- package/dist/manifest.d.ts.map +1 -0
- package/dist/manifest.js +22 -0
- package/dist/nav.d.ts +53 -0
- package/dist/nav.d.ts.map +1 -0
- package/dist/nav.js +25 -0
- package/dist/pagination.d.ts +28 -0
- package/dist/pagination.d.ts.map +1 -0
- package/dist/pagination.js +27 -0
- package/dist/registry.d.ts +183 -0
- package/dist/registry.d.ts.map +1 -0
- package/dist/registry.js +24 -0
- package/dist/roles.d.ts +215 -0
- package/dist/roles.d.ts.map +1 -0
- package/dist/roles.js +45 -0
- package/dist/scopes.d.ts +220 -0
- package/dist/scopes.d.ts.map +1 -0
- package/dist/scopes.js +108 -0
- package/dist/service-accounts.d.ts +81 -0
- package/dist/service-accounts.d.ts.map +1 -0
- package/dist/service-accounts.js +35 -0
- package/dist/tsconfig.lib.tsbuildinfo +1 -0
- package/dist/type-assertions.d.ts +18 -0
- package/dist/type-assertions.d.ts.map +1 -0
- package/dist/type-assertions.js +8 -0
- package/dist/users.d.ts +364 -0
- package/dist/users.d.ts.map +1 -0
- package/dist/users.js +134 -0
- package/package.json +39 -0
package/dist/grants.js
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Authorization contract — the resolved WHO × WHAT × WHERE answer every module
|
|
3
|
+
* consumes (Doc 04 §4–6).
|
|
4
|
+
*/
|
|
5
|
+
/** Redis key prefix for cached grants (Doc 04 §6). */
|
|
6
|
+
export const GRANTS_CACHE_KEY_PREFIX = 'perms';
|
|
7
|
+
/**
|
|
8
|
+
* `perms:{clientId}:{subjectType}:{subjectId}` — the cache key shape from
|
|
9
|
+
* Doc 04 §6. A function so IAM and every cache holder derive it identically.
|
|
10
|
+
*/
|
|
11
|
+
export function grantsCacheKey(clientId, subjectType, subjectId) {
|
|
12
|
+
return `${GRANTS_CACHE_KEY_PREFIX}:${clientId}:${subjectType}:${subjectId}`;
|
|
13
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@plantops/contracts` — the public contract of the PlantOps IAM (Doc 08 §3).
|
|
3
|
+
*
|
|
4
|
+
* Framework-agnostic types and constants only, with **zero dependencies**, so
|
|
5
|
+
* every app, lib, and future operational module can import it without pulling
|
|
6
|
+
* anything else in. Keep it stable: changes here ripple to every consumer.
|
|
7
|
+
*/
|
|
8
|
+
export * from './audit.js';
|
|
9
|
+
export * from './bindings.js';
|
|
10
|
+
export * from './clients.js';
|
|
11
|
+
export * from './constants.js';
|
|
12
|
+
export * from './entitlements.js';
|
|
13
|
+
export * from './errors.js';
|
|
14
|
+
export * from './grants.js';
|
|
15
|
+
export * from './jwt.js';
|
|
16
|
+
export * from './manifest.js';
|
|
17
|
+
export * from './nav.js';
|
|
18
|
+
export * from './pagination.js';
|
|
19
|
+
export * from './registry.js';
|
|
20
|
+
export * from './roles.js';
|
|
21
|
+
export * from './scopes.js';
|
|
22
|
+
export * from './service-accounts.js';
|
|
23
|
+
export * from './users.js';
|
|
24
|
+
/**
|
|
25
|
+
* The compile-time assertion helpers (Doc 08 §7).
|
|
26
|
+
*
|
|
27
|
+
* Type-only and therefore free at runtime, and exported from the barrel since
|
|
28
|
+
* Session H6: `iam-api`'s `openapi/schemas.spec.ts` pins every response schema
|
|
29
|
+
* to the interface above it with `Expect<Equal<…>>`, and a helper that a second
|
|
30
|
+
* workspace consumer needs belongs on the public surface rather than behind a
|
|
31
|
+
* deep import.
|
|
32
|
+
*/
|
|
33
|
+
export * from './type-assertions.js';
|
|
34
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,cAAc,YAAY,CAAC;AAC3B,cAAc,eAAe,CAAC;AAC9B,cAAc,cAAc,CAAC;AAC7B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,mBAAmB,CAAC;AAClC,cAAc,aAAa,CAAC;AAC5B,cAAc,aAAa,CAAC;AAC5B,cAAc,UAAU,CAAC;AACzB,cAAc,eAAe,CAAC;AAC9B,cAAc,UAAU,CAAC;AACzB,cAAc,iBAAiB,CAAC;AAChC,cAAc,eAAe,CAAC;AAC9B,cAAc,YAAY,CAAC;AAC3B,cAAc,aAAa,CAAC;AAC5B,cAAc,uBAAuB,CAAC;AACtC,cAAc,YAAY,CAAC;AAE3B;;;;;;;;GAQG;AACH,cAAc,sBAAsB,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@plantops/contracts` — the public contract of the PlantOps IAM (Doc 08 §3).
|
|
3
|
+
*
|
|
4
|
+
* Framework-agnostic types and constants only, with **zero dependencies**, so
|
|
5
|
+
* every app, lib, and future operational module can import it without pulling
|
|
6
|
+
* anything else in. Keep it stable: changes here ripple to every consumer.
|
|
7
|
+
*/
|
|
8
|
+
export * from './audit.js';
|
|
9
|
+
export * from './bindings.js';
|
|
10
|
+
export * from './clients.js';
|
|
11
|
+
export * from './constants.js';
|
|
12
|
+
export * from './entitlements.js';
|
|
13
|
+
export * from './errors.js';
|
|
14
|
+
export * from './grants.js';
|
|
15
|
+
export * from './jwt.js';
|
|
16
|
+
export * from './manifest.js';
|
|
17
|
+
export * from './nav.js';
|
|
18
|
+
export * from './pagination.js';
|
|
19
|
+
export * from './registry.js';
|
|
20
|
+
export * from './roles.js';
|
|
21
|
+
export * from './scopes.js';
|
|
22
|
+
export * from './service-accounts.js';
|
|
23
|
+
export * from './users.js';
|
|
24
|
+
/**
|
|
25
|
+
* The compile-time assertion helpers (Doc 08 §7).
|
|
26
|
+
*
|
|
27
|
+
* Type-only and therefore free at runtime, and exported from the barrel since
|
|
28
|
+
* Session H6: `iam-api`'s `openapi/schemas.spec.ts` pins every response schema
|
|
29
|
+
* to the interface above it with `Expect<Equal<…>>`, and a helper that a second
|
|
30
|
+
* workspace consumer needs belongs on the public surface rather than behind a
|
|
31
|
+
* deep import.
|
|
32
|
+
*/
|
|
33
|
+
export * from './type-assertions.js';
|
package/dist/jwt.d.ts
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Identity contract — the access-token claim shape (Doc 03 §2) and the token
|
|
3
|
+
* responses `/auth/*` returns (Doc 06 §3).
|
|
4
|
+
*/
|
|
5
|
+
/** WHO: a human `user` or a machine `service_account` (Doc 00 §2). */
|
|
6
|
+
export declare const SubjectType: {
|
|
7
|
+
readonly USER: "user";
|
|
8
|
+
readonly SERVICE: "service";
|
|
9
|
+
};
|
|
10
|
+
export type SubjectType = (typeof SubjectType)[keyof typeof SubjectType];
|
|
11
|
+
/**
|
|
12
|
+
* Access-token claims — exactly these seven, nothing more (Doc 03 §2).
|
|
13
|
+
*
|
|
14
|
+
* Permissions, roles and scope nodes are deliberately absent: they are resolved
|
|
15
|
+
* from `/iam/permissions/resolve` and cached, so a grant change takes effect on
|
|
16
|
+
* invalidation rather than on token expiry.
|
|
17
|
+
*
|
|
18
|
+
* Platform-admin status is *not* a claim either — it is derived from the
|
|
19
|
+
* subject's binding at the platform scope root (Doc 04 §10).
|
|
20
|
+
*/
|
|
21
|
+
export interface JwtClaims {
|
|
22
|
+
/** Issuer — always {@link IAM_ISSUER}. */
|
|
23
|
+
iss: string;
|
|
24
|
+
/** Subject: `user.id` or `service_account.id`. */
|
|
25
|
+
sub: string;
|
|
26
|
+
/** Subject type. */
|
|
27
|
+
sty: SubjectType;
|
|
28
|
+
/** Tenant — `client_id`. The *only* trustworthy source of tenant (Doc 07 §5). */
|
|
29
|
+
cid: string;
|
|
30
|
+
/** Session id, for revocation. Ephemeral for service tokens (Doc 03 §5). */
|
|
31
|
+
sid: string;
|
|
32
|
+
/** Issued-at, epoch seconds. */
|
|
33
|
+
iat: number;
|
|
34
|
+
/** Expiry, epoch seconds. */
|
|
35
|
+
exp: number;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* The claim names an access token may carry, in Doc 03 §2 order. Used by the
|
|
39
|
+
* token service's shape test — an extra claim is a spec violation, not a
|
|
40
|
+
* harmless addition.
|
|
41
|
+
*/
|
|
42
|
+
export declare const JWT_CLAIM_KEYS: readonly ["iss", "sub", "sty", "cid", "sid", "iat", "exp"];
|
|
43
|
+
export type JwtClaimKey = (typeof JWT_CLAIM_KEYS)[number];
|
|
44
|
+
/**
|
|
45
|
+
* Claims a verifier **accepts without requiring or interpreting** (ADR 0005 §5.1).
|
|
46
|
+
*
|
|
47
|
+
* Not part of {@link JwtClaims}, and deliberately not: nothing reads these, no
|
|
48
|
+
* decision branches on them, and `readAccessTokenClaims` drops them rather than
|
|
49
|
+
* returning them. A caller cannot accidentally consume one.
|
|
50
|
+
*
|
|
51
|
+
* ## Why `aud` is here while the IAM mints no `aud`
|
|
52
|
+
*
|
|
53
|
+
* The claim set is closed in both directions (Doc 03 §2), and the *accepting*
|
|
54
|
+
* half of that is the half this repository cannot migrate. Every consumer
|
|
55
|
+
* verifies locally with the published `@plantops/auth-kit`, so the day the IAM
|
|
56
|
+
* mints a claim an older verifier has never heard of, that verifier rejects
|
|
57
|
+
* **every** token — a total outage rather than a degradation.
|
|
58
|
+
*
|
|
59
|
+
* Admitting `aud` now costs nothing today and buys the option to reconsider ADR
|
|
60
|
+
* 0005 §3.3 later without a strictly-ordered upgrade across repositories this
|
|
61
|
+
* one does not control. After `libs-v0.1.0` the option is gone, which is why
|
|
62
|
+
* ADR 0005 §5.1 gave it a deadline rather than a backlog entry.
|
|
63
|
+
*
|
|
64
|
+
* Signing is **not** relaxed: `assertExactClaims` still refuses an eighth claim,
|
|
65
|
+
* so this changes what a token may contain, not what this IAM puts in one. The
|
|
66
|
+
* asymmetry is the whole design — the verifier is un-migratable, the signer is
|
|
67
|
+
* one deploy.
|
|
68
|
+
*/
|
|
69
|
+
export declare const JWT_TOLERATED_CLAIM_KEYS: readonly ["aud"];
|
|
70
|
+
export type JwtToleratedClaimKey = (typeof JWT_TOLERATED_CLAIM_KEYS)[number];
|
|
71
|
+
/** `POST /auth/login` body (Doc 06 §3). */
|
|
72
|
+
export interface LoginRequest {
|
|
73
|
+
email: string;
|
|
74
|
+
password: string;
|
|
75
|
+
client_slug: string;
|
|
76
|
+
}
|
|
77
|
+
/** `POST /auth/refresh` body (Doc 06 §3). */
|
|
78
|
+
export interface RefreshRequest {
|
|
79
|
+
refresh_token: string;
|
|
80
|
+
}
|
|
81
|
+
/** `POST /auth/token` body — client-credentials exchange (Doc 03 §5). */
|
|
82
|
+
export interface ServiceTokenRequest {
|
|
83
|
+
account_key: string;
|
|
84
|
+
account_secret: string;
|
|
85
|
+
}
|
|
86
|
+
/** Login / refresh response (Doc 06 §3). */
|
|
87
|
+
export interface TokenPairResponse {
|
|
88
|
+
access_token: string;
|
|
89
|
+
refresh_token: string;
|
|
90
|
+
/** Access-token lifetime in seconds. */
|
|
91
|
+
expires_in: number;
|
|
92
|
+
}
|
|
93
|
+
/** Service-account token response — no refresh token by design (Doc 03 §5). */
|
|
94
|
+
export interface AccessTokenResponse {
|
|
95
|
+
access_token: string;
|
|
96
|
+
expires_in: number;
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* One row of `GET /auth/sessions` (Doc 06 §3).
|
|
100
|
+
*
|
|
101
|
+
* Note what is absent: `refresh_token_hash`. A session list is a security
|
|
102
|
+
* screen — "where am I logged in, and what do I want to kill" — and it is
|
|
103
|
+
* rendered in a browser. The hash is the credential's stored form; nothing that
|
|
104
|
+
* can be replayed or brute-forced belongs in a response that exists to *show*
|
|
105
|
+
* sessions (Doc 10 §8).
|
|
106
|
+
*/
|
|
107
|
+
export interface SessionDTO {
|
|
108
|
+
/** The `sid` claim of tokens issued for this session. */
|
|
109
|
+
id: string;
|
|
110
|
+
/** e.g. "Gate-3 Terminal" — what makes the list actionable. */
|
|
111
|
+
device_label: string | null;
|
|
112
|
+
/** ISO-8601. */
|
|
113
|
+
issued_at: string;
|
|
114
|
+
expires_at: string;
|
|
115
|
+
/** Set once revoked, and never cleared. */
|
|
116
|
+
revoked_at: string | null;
|
|
117
|
+
/**
|
|
118
|
+
* True for the session the calling token belongs to.
|
|
119
|
+
*
|
|
120
|
+
* Without it a user revoking "everything else" has no way to tell which row
|
|
121
|
+
* is the browser they are looking at — and logging yourself out while trying
|
|
122
|
+
* to log a lost terminal out is the mistake this flag exists to prevent.
|
|
123
|
+
*/
|
|
124
|
+
current: boolean;
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* `POST /iam/introspect` response (Doc 06 §11). Inactive tokens carry no
|
|
128
|
+
* identity fields — never leak subject data for a token that failed
|
|
129
|
+
* verification.
|
|
130
|
+
*/
|
|
131
|
+
export type IntrospectResponse = {
|
|
132
|
+
active: false;
|
|
133
|
+
} | {
|
|
134
|
+
active: true;
|
|
135
|
+
sub: string;
|
|
136
|
+
sty: SubjectType;
|
|
137
|
+
cid: string;
|
|
138
|
+
sid: string;
|
|
139
|
+
};
|
|
140
|
+
/** One entry of `/iam/.well-known/jwks.json` (Doc 03 §1). */
|
|
141
|
+
export interface JwkDTO {
|
|
142
|
+
kty: string;
|
|
143
|
+
use?: string;
|
|
144
|
+
alg?: string;
|
|
145
|
+
kid: string;
|
|
146
|
+
n?: string;
|
|
147
|
+
e?: string;
|
|
148
|
+
crv?: string;
|
|
149
|
+
x?: string;
|
|
150
|
+
y?: string;
|
|
151
|
+
}
|
|
152
|
+
export interface JwksResponse {
|
|
153
|
+
keys: JwkDTO[];
|
|
154
|
+
}
|
|
155
|
+
//# sourceMappingURL=jwt.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"jwt.d.ts","sourceRoot":"","sources":["../src/jwt.ts"],"names":[],"mappings":"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"}
|
package/dist/jwt.js
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Identity contract — the access-token claim shape (Doc 03 §2) and the token
|
|
3
|
+
* responses `/auth/*` returns (Doc 06 §3).
|
|
4
|
+
*/
|
|
5
|
+
/** WHO: a human `user` or a machine `service_account` (Doc 00 §2). */
|
|
6
|
+
export const SubjectType = {
|
|
7
|
+
USER: 'user',
|
|
8
|
+
SERVICE: 'service',
|
|
9
|
+
};
|
|
10
|
+
/**
|
|
11
|
+
* The claim names an access token may carry, in Doc 03 §2 order. Used by the
|
|
12
|
+
* token service's shape test — an extra claim is a spec violation, not a
|
|
13
|
+
* harmless addition.
|
|
14
|
+
*/
|
|
15
|
+
export const JWT_CLAIM_KEYS = [
|
|
16
|
+
'iss',
|
|
17
|
+
'sub',
|
|
18
|
+
'sty',
|
|
19
|
+
'cid',
|
|
20
|
+
'sid',
|
|
21
|
+
'iat',
|
|
22
|
+
'exp',
|
|
23
|
+
];
|
|
24
|
+
/**
|
|
25
|
+
* Claims a verifier **accepts without requiring or interpreting** (ADR 0005 §5.1).
|
|
26
|
+
*
|
|
27
|
+
* Not part of {@link JwtClaims}, and deliberately not: nothing reads these, no
|
|
28
|
+
* decision branches on them, and `readAccessTokenClaims` drops them rather than
|
|
29
|
+
* returning them. A caller cannot accidentally consume one.
|
|
30
|
+
*
|
|
31
|
+
* ## Why `aud` is here while the IAM mints no `aud`
|
|
32
|
+
*
|
|
33
|
+
* The claim set is closed in both directions (Doc 03 §2), and the *accepting*
|
|
34
|
+
* half of that is the half this repository cannot migrate. Every consumer
|
|
35
|
+
* verifies locally with the published `@plantops/auth-kit`, so the day the IAM
|
|
36
|
+
* mints a claim an older verifier has never heard of, that verifier rejects
|
|
37
|
+
* **every** token — a total outage rather than a degradation.
|
|
38
|
+
*
|
|
39
|
+
* Admitting `aud` now costs nothing today and buys the option to reconsider ADR
|
|
40
|
+
* 0005 §3.3 later without a strictly-ordered upgrade across repositories this
|
|
41
|
+
* one does not control. After `libs-v0.1.0` the option is gone, which is why
|
|
42
|
+
* ADR 0005 §5.1 gave it a deadline rather than a backlog entry.
|
|
43
|
+
*
|
|
44
|
+
* Signing is **not** relaxed: `assertExactClaims` still refuses an eighth claim,
|
|
45
|
+
* so this changes what a token may contain, not what this IAM puts in one. The
|
|
46
|
+
* asymmetry is the whole design — the verifier is un-migratable, the signer is
|
|
47
|
+
* one deploy.
|
|
48
|
+
*/
|
|
49
|
+
export const JWT_TOLERATED_CLAIM_KEYS = ['aud'];
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"contracts.d.ts","sourceRoot":"","sources":["../../src/lib/contracts.ts"],"names":[],"mappings":"AAAA,wBAAgB,SAAS,IAAI,MAAM,CAElC"}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Application manifest — the declarative document an application ships to
|
|
3
|
+
* register (and later evolve) its permission and navigation catalog
|
|
4
|
+
* (Doc 02 §2).
|
|
5
|
+
*
|
|
6
|
+
* Uploading a manifest is an **upsert keyed by natural key** (`application.key`
|
|
7
|
+
* + node/permission `key`): re-uploading is idempotent, changed labels update in
|
|
8
|
+
* place, and keys absent from a re-upload soft-deactivate rather than delete
|
|
9
|
+
* (Doc 02 §7).
|
|
10
|
+
*
|
|
11
|
+
* ## Two naming conventions in one file, on purpose
|
|
12
|
+
*
|
|
13
|
+
* The manifest *document* is camelCase (`sortOrder`, `isPublic`). It is a
|
|
14
|
+
* hand-authored JSON file that ships with an application, not an API body, and
|
|
15
|
+
* this shape is the one the contract has published since Session 1.
|
|
16
|
+
*
|
|
17
|
+
* The *response* types below are snake_case, matching {@link ApplicationDTO} and
|
|
18
|
+
* every other shape Doc 06 §3 writes out. A convention that changes between two
|
|
19
|
+
* endpoints of the same API is worse than either convention — and the diff is an
|
|
20
|
+
* API response, whatever the document that produced it looks like.
|
|
21
|
+
*/
|
|
22
|
+
import type { NavNodeKind } from './nav.js';
|
|
23
|
+
export interface ManifestPermission {
|
|
24
|
+
/** Namespaced `app.resource.action`, unique within the application. */
|
|
25
|
+
key: string;
|
|
26
|
+
name: string;
|
|
27
|
+
description?: string;
|
|
28
|
+
}
|
|
29
|
+
export interface ManifestNavNode {
|
|
30
|
+
kind: NavNodeKind;
|
|
31
|
+
/** Unique within the application; the upsert's natural key. */
|
|
32
|
+
key: string;
|
|
33
|
+
label: string;
|
|
34
|
+
/** Frontend path. Omit for pure containers. */
|
|
35
|
+
route?: string;
|
|
36
|
+
/** Icon key resolved by the frontend's icon set (Doc 05 §7). */
|
|
37
|
+
icon?: string;
|
|
38
|
+
sortOrder?: number;
|
|
39
|
+
/**
|
|
40
|
+
* Leaf-only opt-in making an *unmapped* leaf visible to anyone who can see
|
|
41
|
+
* the app. Defaults to `false` — unmapped means hidden (Doc 05 §3).
|
|
42
|
+
*/
|
|
43
|
+
isPublic?: boolean;
|
|
44
|
+
/**
|
|
45
|
+
* Permission keys gating this node, OR semantics — becomes `menu_permission`
|
|
46
|
+
* rows (Doc 01 §4.4).
|
|
47
|
+
*/
|
|
48
|
+
requires?: string[];
|
|
49
|
+
children?: ManifestNavNode[];
|
|
50
|
+
}
|
|
51
|
+
export interface ApplicationManifest {
|
|
52
|
+
/** Machine key of the application, e.g. `gatepass` (Doc 01 §3.1). */
|
|
53
|
+
key: string;
|
|
54
|
+
name: string;
|
|
55
|
+
description?: string;
|
|
56
|
+
permissions: ManifestPermission[];
|
|
57
|
+
nav: ManifestNavNode[];
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* What an upsert did to one kind of catalog row, by natural key.
|
|
61
|
+
*
|
|
62
|
+
* Keys rather than counts, because both consumers need the names: Doc 09 §2.1's
|
|
63
|
+
* preview screen shows the operator *what* is about to change, and the audit
|
|
64
|
+
* payload has to answer "when did this key appear, and when did it go" long
|
|
65
|
+
* after the manifest that caused it was overwritten.
|
|
66
|
+
*
|
|
67
|
+
* There is no `deleted`. Removal from a manifest soft-deactivates (Doc 02 §7) —
|
|
68
|
+
* `role_permission` rows and audit payloads still name these keys, and a hard
|
|
69
|
+
* delete would revoke grants with no record of what was revoked.
|
|
70
|
+
*
|
|
71
|
+
* Reactivation of a key that came back is an `updated`, with `is_active` among
|
|
72
|
+
* the fields that moved.
|
|
73
|
+
*/
|
|
74
|
+
export interface ManifestEntityDiff {
|
|
75
|
+
created: string[];
|
|
76
|
+
updated: string[];
|
|
77
|
+
deactivated: string[];
|
|
78
|
+
}
|
|
79
|
+
/** One nav node and the permission keys mapped to (or unmapped from) it. */
|
|
80
|
+
export interface ManifestMappingChange {
|
|
81
|
+
nav_key: string;
|
|
82
|
+
permission_keys: string[];
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* `menu_permission` movement, grouped by nav node.
|
|
86
|
+
*
|
|
87
|
+
* Grouped rather than listed pair by pair for the reason the mapping audit
|
|
88
|
+
* records are: a node's gate is one fact about that node, and a flat pair list
|
|
89
|
+
* says the same thing in `nodes × permissions` lines.
|
|
90
|
+
*/
|
|
91
|
+
export interface ManifestMappingDiff {
|
|
92
|
+
mapped: ManifestMappingChange[];
|
|
93
|
+
unmapped: ManifestMappingChange[];
|
|
94
|
+
}
|
|
95
|
+
/** Fields of the `application` row itself the manifest moved (`name`, `description`). */
|
|
96
|
+
export interface ManifestApplicationDiff {
|
|
97
|
+
key: string;
|
|
98
|
+
changed: string[];
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* The complete difference between a manifest and the catalog it is applied to.
|
|
102
|
+
*
|
|
103
|
+
* Computed before anything is written, which is what lets Session 29 preview it
|
|
104
|
+
* and this session audit it — the same value, from the same function.
|
|
105
|
+
*/
|
|
106
|
+
export interface ManifestDiff {
|
|
107
|
+
application: ManifestApplicationDiff;
|
|
108
|
+
permissions: ManifestEntityDiff;
|
|
109
|
+
nav: ManifestEntityDiff;
|
|
110
|
+
menu_permissions: ManifestMappingDiff;
|
|
111
|
+
}
|
|
112
|
+
/** `POST /iam/applications/:id/manifest[?dryRun=true]` (Doc 06 §4). */
|
|
113
|
+
export interface ManifestUpsertResponse {
|
|
114
|
+
application_id: string;
|
|
115
|
+
/**
|
|
116
|
+
* `true` when the call only *computed* the diff — `?dryRun=true`, the preview
|
|
117
|
+
* Doc 09 §2.1 shows an operator before they confirm an upload.
|
|
118
|
+
*
|
|
119
|
+
* A preview writes nothing: no catalog rows, no audit record, no cache
|
|
120
|
+
* invalidation. It is otherwise the same answer the real upload gives, from
|
|
121
|
+
* the same function against the same snapshot, which is what lets the screen
|
|
122
|
+
* promise that confirming applies what was previewed.
|
|
123
|
+
*/
|
|
124
|
+
dry_run: boolean;
|
|
125
|
+
/**
|
|
126
|
+
* `false` when the manifest described exactly what was already there — and,
|
|
127
|
+
* on a dry run, that applying it *would* change nothing.
|
|
128
|
+
*
|
|
129
|
+
* A no-op upsert writes nothing at all — no rows, no audit record — so this
|
|
130
|
+
* flag is the only way a caller can tell an idempotent re-upload from a
|
|
131
|
+
* first one without diffing the diff itself.
|
|
132
|
+
*/
|
|
133
|
+
changed: boolean;
|
|
134
|
+
diff: ManifestDiff;
|
|
135
|
+
}
|
|
136
|
+
//# sourceMappingURL=manifest.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"manifest.d.ts","sourceRoot":"","sources":["../src/manifest.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,UAAU,CAAC;AAE5C,MAAM,WAAW,kBAAkB;IACjC,uEAAuE;IACvE,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,WAAW,CAAC;IAClB,+DAA+D;IAC/D,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,MAAM,CAAC;IACd,+CAA+C;IAC/C,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,gEAAgE;IAChE,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;OAGG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB;;;OAGG;IACH,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;IACpB,QAAQ,CAAC,EAAE,eAAe,EAAE,CAAC;CAC9B;AAED,MAAM,WAAW,mBAAmB;IAClC,qEAAqE;IACrE,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,WAAW,EAAE,kBAAkB,EAAE,CAAC;IAClC,GAAG,EAAE,eAAe,EAAE,CAAC;CACxB;AAED;;;;;;;;;;;;;;GAcG;AACH,MAAM,WAAW,kBAAkB;IACjC,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,WAAW,EAAE,MAAM,EAAE,CAAC;CACvB;AAED,4EAA4E;AAC5E,MAAM,WAAW,qBAAqB;IACpC,OAAO,EAAE,MAAM,CAAC;IAChB,eAAe,EAAE,MAAM,EAAE,CAAC;CAC3B;AAED;;;;;;GAMG;AACH,MAAM,WAAW,mBAAmB;IAClC,MAAM,EAAE,qBAAqB,EAAE,CAAC;IAChC,QAAQ,EAAE,qBAAqB,EAAE,CAAC;CACnC;AAED,yFAAyF;AACzF,MAAM,WAAW,uBAAuB;IACtC,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,EAAE,MAAM,EAAE,CAAC;CACnB;AAED;;;;;GAKG;AACH,MAAM,WAAW,YAAY;IAC3B,WAAW,EAAE,uBAAuB,CAAC;IACrC,WAAW,EAAE,kBAAkB,CAAC;IAChC,GAAG,EAAE,kBAAkB,CAAC;IACxB,gBAAgB,EAAE,mBAAmB,CAAC;CACvC;AAED,uEAAuE;AACvE,MAAM,WAAW,sBAAsB;IACrC,cAAc,EAAE,MAAM,CAAC;IACvB;;;;;;;;OAQG;IACH,OAAO,EAAE,OAAO,CAAC;IACjB;;;;;;;OAOG;IACH,OAAO,EAAE,OAAO,CAAC;IACjB,IAAI,EAAE,YAAY,CAAC;CACpB"}
|
package/dist/manifest.js
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Application manifest — the declarative document an application ships to
|
|
3
|
+
* register (and later evolve) its permission and navigation catalog
|
|
4
|
+
* (Doc 02 §2).
|
|
5
|
+
*
|
|
6
|
+
* Uploading a manifest is an **upsert keyed by natural key** (`application.key`
|
|
7
|
+
* + node/permission `key`): re-uploading is idempotent, changed labels update in
|
|
8
|
+
* place, and keys absent from a re-upload soft-deactivate rather than delete
|
|
9
|
+
* (Doc 02 §7).
|
|
10
|
+
*
|
|
11
|
+
* ## Two naming conventions in one file, on purpose
|
|
12
|
+
*
|
|
13
|
+
* The manifest *document* is camelCase (`sortOrder`, `isPublic`). It is a
|
|
14
|
+
* hand-authored JSON file that ships with an application, not an API body, and
|
|
15
|
+
* this shape is the one the contract has published since Session 1.
|
|
16
|
+
*
|
|
17
|
+
* The *response* types below are snake_case, matching {@link ApplicationDTO} and
|
|
18
|
+
* every other shape Doc 06 §3 writes out. A convention that changes between two
|
|
19
|
+
* endpoints of the same API is worse than either convention — and the diff is an
|
|
20
|
+
* API response, whatever the document that produced it looks like.
|
|
21
|
+
*/
|
|
22
|
+
export {};
|
package/dist/nav.d.ts
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Navigation contract — the pruned menu tree `GET /iam/navigation` returns
|
|
3
|
+
* (Doc 05 §4). The frontend renders this directly; it keeps no menu constants
|
|
4
|
+
* of its own (Doc 05 §7).
|
|
5
|
+
*/
|
|
6
|
+
/** Depth discriminator on the self-referencing `nav_node` table (Doc 01 §3.3). */
|
|
7
|
+
export declare const NavNodeKind: {
|
|
8
|
+
readonly MODULE: "module";
|
|
9
|
+
readonly MENU: "menu";
|
|
10
|
+
readonly SUB_MENU: "sub_menu";
|
|
11
|
+
};
|
|
12
|
+
export type NavNodeKind = (typeof NavNodeKind)[keyof typeof NavNodeKind];
|
|
13
|
+
/**
|
|
14
|
+
* The three kinds as a list, in the order the Postgres enum declares its labels
|
|
15
|
+
* (migration 0001).
|
|
16
|
+
*
|
|
17
|
+
* Exists for the same reason `SERVICE_ACCOUNT_STATUS_VALUES` does: a request
|
|
18
|
+
* schema needs an array to build a closed enum from, and deriving it from the
|
|
19
|
+
* object above with `Object.values` would lose the tuple type that makes
|
|
20
|
+
* `z.enum` produce a `NavNodeKind` rather than a `string`.
|
|
21
|
+
*/
|
|
22
|
+
export declare const NAV_NODE_KIND_VALUES: readonly ["module", "menu", "sub_menu"];
|
|
23
|
+
/**
|
|
24
|
+
* A node in the resolved tree. Only visible nodes are present: leaves the
|
|
25
|
+
* subject holds no mapped permission for are pruned, and containers left
|
|
26
|
+
* without a visible descendant are pruned with them (Doc 05 §3).
|
|
27
|
+
*/
|
|
28
|
+
export interface NavNodeDTO {
|
|
29
|
+
id: string;
|
|
30
|
+
kind: NavNodeKind;
|
|
31
|
+
key: string;
|
|
32
|
+
label: string;
|
|
33
|
+
/** Frontend path; absent on pure containers. */
|
|
34
|
+
route?: string | null;
|
|
35
|
+
/** Icon *key* — the frontend maps it to its own icon set (Doc 05 §7). */
|
|
36
|
+
icon?: string | null;
|
|
37
|
+
children: NavNodeDTO[];
|
|
38
|
+
}
|
|
39
|
+
export interface ApplicationSummaryDTO {
|
|
40
|
+
id: string;
|
|
41
|
+
key: string;
|
|
42
|
+
name: string;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* `GET /iam/navigation?applicationId=` → one application's tree.
|
|
46
|
+
* `GET /iam/navigation` (no id) → the cross-application shell, one top-level
|
|
47
|
+
* node per enabled application, with `application: null` (Doc 05 §4).
|
|
48
|
+
*/
|
|
49
|
+
export interface NavigationResponse {
|
|
50
|
+
application: ApplicationSummaryDTO | null;
|
|
51
|
+
tree: NavNodeDTO[];
|
|
52
|
+
}
|
|
53
|
+
//# sourceMappingURL=nav.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"nav.d.ts","sourceRoot":"","sources":["../src/nav.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,kFAAkF;AAClF,eAAO,MAAM,WAAW;;;;CAId,CAAC;AACX,MAAM,MAAM,WAAW,GAAG,CAAC,OAAO,WAAW,CAAC,CAAC,MAAM,OAAO,WAAW,CAAC,CAAC;AAEzE;;;;;;;;GAQG;AACH,eAAO,MAAM,oBAAoB,yCAIU,CAAC;AAE5C;;;;GAIG;AACH,MAAM,WAAW,UAAU;IACzB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,WAAW,CAAC;IAClB,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,MAAM,CAAC;IACd,gDAAgD;IAChD,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,yEAAyE;IACzE,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,QAAQ,EAAE,UAAU,EAAE,CAAC;CACxB;AAED,MAAM,WAAW,qBAAqB;IACpC,EAAE,EAAE,MAAM,CAAC;IACX,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;CACd;AAED;;;;GAIG;AACH,MAAM,WAAW,kBAAkB;IACjC,WAAW,EAAE,qBAAqB,GAAG,IAAI,CAAC;IAC1C,IAAI,EAAE,UAAU,EAAE,CAAC;CACpB"}
|
package/dist/nav.js
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Navigation contract — the pruned menu tree `GET /iam/navigation` returns
|
|
3
|
+
* (Doc 05 §4). The frontend renders this directly; it keeps no menu constants
|
|
4
|
+
* of its own (Doc 05 §7).
|
|
5
|
+
*/
|
|
6
|
+
/** Depth discriminator on the self-referencing `nav_node` table (Doc 01 §3.3). */
|
|
7
|
+
export const NavNodeKind = {
|
|
8
|
+
MODULE: 'module',
|
|
9
|
+
MENU: 'menu',
|
|
10
|
+
SUB_MENU: 'sub_menu',
|
|
11
|
+
};
|
|
12
|
+
/**
|
|
13
|
+
* The three kinds as a list, in the order the Postgres enum declares its labels
|
|
14
|
+
* (migration 0001).
|
|
15
|
+
*
|
|
16
|
+
* Exists for the same reason `SERVICE_ACCOUNT_STATUS_VALUES` does: a request
|
|
17
|
+
* schema needs an array to build a closed enum from, and deriving it from the
|
|
18
|
+
* object above with `Object.values` would lose the tuple type that makes
|
|
19
|
+
* `z.enum` produce a `NavNodeKind` rather than a `string`.
|
|
20
|
+
*/
|
|
21
|
+
export const NAV_NODE_KIND_VALUES = [
|
|
22
|
+
NavNodeKind.MODULE,
|
|
23
|
+
NavNodeKind.MENU,
|
|
24
|
+
NavNodeKind.SUB_MENU,
|
|
25
|
+
];
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pagination contract — `?page=&limit=` → `{ data, page, limit, total }`
|
|
3
|
+
* (Doc 06 §1).
|
|
4
|
+
*
|
|
5
|
+
* Note the deliberate exception: `/iam/permissions/resolve` is **not**
|
|
6
|
+
* paginated. A subject's grant set is one cacheable unit, bounded instead by
|
|
7
|
+
* path minimization and the optional `applicationId` filter (Doc 06 §11).
|
|
8
|
+
*/
|
|
9
|
+
export declare const DEFAULT_PAGE = 1;
|
|
10
|
+
export declare const DEFAULT_PAGE_SIZE = 25;
|
|
11
|
+
export declare const MAX_PAGE_SIZE = 100;
|
|
12
|
+
/** Query parameters accepted by every list endpoint. */
|
|
13
|
+
export interface PaginationQuery {
|
|
14
|
+
page?: number;
|
|
15
|
+
limit?: number;
|
|
16
|
+
}
|
|
17
|
+
/** The envelope every list endpoint returns. */
|
|
18
|
+
export interface Paginated<T> {
|
|
19
|
+
data: T[];
|
|
20
|
+
page: number;
|
|
21
|
+
limit: number;
|
|
22
|
+
/** Total matching rows, not the length of `data`. */
|
|
23
|
+
total: number;
|
|
24
|
+
}
|
|
25
|
+
/** Normalizes user input to a valid, bounded `(page, limit)` pair. */
|
|
26
|
+
export declare function normalizePagination(query?: PaginationQuery): Required<PaginationQuery>;
|
|
27
|
+
export declare function paginated<T>(data: T[], total: number, query?: PaginationQuery): Paginated<T>;
|
|
28
|
+
//# sourceMappingURL=pagination.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pagination.d.ts","sourceRoot":"","sources":["../src/pagination.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,eAAO,MAAM,YAAY,IAAI,CAAC;AAC9B,eAAO,MAAM,iBAAiB,KAAK,CAAC;AACpC,eAAO,MAAM,aAAa,MAAM,CAAC;AAEjC,wDAAwD;AACxD,MAAM,WAAW,eAAe;IAC9B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,gDAAgD;AAChD,MAAM,WAAW,SAAS,CAAC,CAAC;IAC1B,IAAI,EAAE,CAAC,EAAE,CAAC;IACV,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,qDAAqD;IACrD,KAAK,EAAE,MAAM,CAAC;CACf;AAED,sEAAsE;AACtE,wBAAgB,mBAAmB,CACjC,KAAK,GAAE,eAAoB,GAC1B,QAAQ,CAAC,eAAe,CAAC,CAK3B;AAQD,wBAAgB,SAAS,CAAC,CAAC,EACzB,IAAI,EAAE,CAAC,EAAE,EACT,KAAK,EAAE,MAAM,EACb,KAAK,GAAE,eAAoB,GAC1B,SAAS,CAAC,CAAC,CAAC,CAGd"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pagination contract — `?page=&limit=` → `{ data, page, limit, total }`
|
|
3
|
+
* (Doc 06 §1).
|
|
4
|
+
*
|
|
5
|
+
* Note the deliberate exception: `/iam/permissions/resolve` is **not**
|
|
6
|
+
* paginated. A subject's grant set is one cacheable unit, bounded instead by
|
|
7
|
+
* path minimization and the optional `applicationId` filter (Doc 06 §11).
|
|
8
|
+
*/
|
|
9
|
+
export const DEFAULT_PAGE = 1;
|
|
10
|
+
export const DEFAULT_PAGE_SIZE = 25;
|
|
11
|
+
export const MAX_PAGE_SIZE = 100;
|
|
12
|
+
/** Normalizes user input to a valid, bounded `(page, limit)` pair. */
|
|
13
|
+
export function normalizePagination(query = {}) {
|
|
14
|
+
const page = Math.max(DEFAULT_PAGE, toInt(query.page, DEFAULT_PAGE));
|
|
15
|
+
const requested = toInt(query.limit, DEFAULT_PAGE_SIZE);
|
|
16
|
+
const limit = Math.min(MAX_PAGE_SIZE, Math.max(1, requested));
|
|
17
|
+
return { page, limit };
|
|
18
|
+
}
|
|
19
|
+
function toInt(value, fallback) {
|
|
20
|
+
return typeof value === 'number' && Number.isFinite(value)
|
|
21
|
+
? Math.trunc(value)
|
|
22
|
+
: fallback;
|
|
23
|
+
}
|
|
24
|
+
export function paginated(data, total, query = {}) {
|
|
25
|
+
const { page, limit } = normalizePagination(query);
|
|
26
|
+
return { data, page, limit, total };
|
|
27
|
+
}
|