@owlmeans/iam 0.1.18-rc.2 → 0.1.18-rc.21

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 (48) hide show
  1. package/README.md +2 -2
  2. package/agent-meta/manifest.json +2 -2
  3. package/agent-meta/skills/iam/SKILL.md +193 -56
  4. package/build/consts.d.ts +91 -0
  5. package/build/consts.d.ts.map +1 -1
  6. package/build/consts.js +97 -0
  7. package/build/consts.js.map +1 -1
  8. package/build/errors.d.ts +13 -0
  9. package/build/errors.d.ts.map +1 -1
  10. package/build/errors.js +17 -0
  11. package/build/errors.js.map +1 -1
  12. package/build/gate/index.d.ts +4 -0
  13. package/build/gate/index.d.ts.map +1 -0
  14. package/build/gate/index.js +4 -0
  15. package/build/gate/index.js.map +1 -0
  16. package/build/gate/param.d.ts +31 -0
  17. package/build/gate/param.d.ts.map +1 -0
  18. package/build/gate/param.js +99 -0
  19. package/build/gate/param.js.map +1 -0
  20. package/build/gate/resolve.d.ts +10 -0
  21. package/build/gate/resolve.d.ts.map +1 -0
  22. package/build/gate/resolve.js +93 -0
  23. package/build/gate/resolve.js.map +1 -0
  24. package/build/gate/validate.d.ts +15 -0
  25. package/build/gate/validate.d.ts.map +1 -0
  26. package/build/gate/validate.js +107 -0
  27. package/build/gate/validate.js.map +1 -0
  28. package/build/index.d.ts +2 -0
  29. package/build/index.d.ts.map +1 -1
  30. package/build/index.js +2 -0
  31. package/build/index.js.map +1 -1
  32. package/build/permission.d.ts +21 -0
  33. package/build/permission.d.ts.map +1 -0
  34. package/build/permission.js +39 -0
  35. package/build/permission.js.map +1 -0
  36. package/build/types.d.ts +251 -6
  37. package/build/types.d.ts.map +1 -1
  38. package/package.json +7 -6
  39. package/src/consts.ts +105 -0
  40. package/src/errors.ts +19 -0
  41. package/src/gate/index.ts +3 -0
  42. package/src/gate/param.ts +122 -0
  43. package/src/gate/resolve.ts +114 -0
  44. package/src/gate/validate.ts +132 -0
  45. package/src/index.ts +2 -0
  46. package/src/permission.ts +48 -0
  47. package/src/types.ts +283 -6
  48. package/tests/gate-param.spec.ts +234 -0
package/build/types.d.ts CHANGED
@@ -1,5 +1,77 @@
1
1
  import type { InitializedService } from '@owlmeans/context';
2
2
  import type { OidcProviderConfig } from '@owlmeans/oidc';
3
+ import type { GateParamSource, GateParamErrorCode, GateResolutionFailure, IamGrantMode, IamRemovalPolicy } from './consts.js';
4
+ /**
5
+ * The parts of a request a gate selector may read.
6
+ *
7
+ * Structural on purpose: `AbstractRequest` is assignable to it, so `@owlmeans/server-iam` passes its
8
+ * request straight through, while tooling outside the server stack can resolve a selector against a
9
+ * plain object without taking a dependency on `@owlmeans/entrypoint`.
10
+ */
11
+ export interface GateRequestLike {
12
+ params?: unknown;
13
+ query?: unknown;
14
+ body?: unknown;
15
+ headers?: unknown;
16
+ auth?: unknown;
17
+ }
18
+ /** Where one gate param reads its resource id from. */
19
+ export interface GateResourceSelector {
20
+ /** The selector text exactly as written, for diagnostics. */
21
+ readonly selector: string;
22
+ /** Undefined for the bare form, which searches `sources` in order. */
23
+ readonly source?: GateParamSource;
24
+ /** Length 1 and UNSPLIT for the bare form — a bare key may legally contain dots. */
25
+ readonly path: string[];
26
+ /** The explicit source, or `DEFAULT_GATE_PARAM_SOURCES`. */
27
+ readonly sources: GateParamSource[];
28
+ }
29
+ export interface GateParamProblem {
30
+ code: GateParamErrorCode;
31
+ detail: string;
32
+ }
33
+ export interface ParsedGateParam {
34
+ permission: string;
35
+ /**
36
+ * @deprecated The bare-form flat key, kept so existing readers keep compiling. Read `resource`,
37
+ * which carries the source and the path for both forms.
38
+ */
39
+ resourceParam?: string;
40
+ resource?: GateResourceSelector;
41
+ error?: GateParamProblem;
42
+ }
43
+ export interface GateResourceResolution {
44
+ id?: string;
45
+ from?: GateParamSource;
46
+ reason?: GateResolutionFailure;
47
+ }
48
+ /** What an entrypoint declares, so a selector can be checked against it before it is ever deployed. */
49
+ export interface GateParamAudit {
50
+ routePath?: string;
51
+ routeParams?: string[];
52
+ filter?: {
53
+ query?: object;
54
+ params?: object;
55
+ body?: object;
56
+ headers?: object;
57
+ };
58
+ }
59
+ export interface GateParamIssue {
60
+ param: string;
61
+ code: GateParamErrorCode;
62
+ detail: string;
63
+ }
64
+ /** A permission NAME taken apart. Never carries a selector — `@` is the gate's syntax alone. */
65
+ export interface ParsedPermissionName {
66
+ /** The whole name, exactly as it must be stored and granted. */
67
+ name: string;
68
+ /** What IAM stores as `resource`. The whole name when there is no action separator. */
69
+ resource: string;
70
+ /** What IAM stores as `action`. Absent for a bare, unsplittable legacy name. */
71
+ action?: string;
72
+ /** Set when the argument carried an `@` — which a NAME never legally does. */
73
+ problem?: GateParamProblem;
74
+ }
3
75
  export interface IamClient {
4
76
  id?: string;
5
77
  clientId: string;
@@ -29,10 +101,32 @@ export interface IamClientOptions {
29
101
  export interface IamPermissionArgs {
30
102
  /** Action name. When absent the permission is unscoped (project-wide). */
31
103
  permission?: string;
32
- /** Declares that grants of this permission may be bound to specific resource ids. */
104
+ /**
105
+ * Declares that grants of this permission may be bound to specific resource ids.
106
+ *
107
+ * It does NOT mean grants must be: the same name is grantable in either form. A grant carrying no
108
+ * resource ids covers every resource, which is what `hasPermission` already implements.
109
+ */
33
110
  resourceScoped?: boolean;
34
111
  /** Optional human-readable title for the permission definition. */
35
112
  title?: string;
113
+ /**
114
+ * Grouping tag — one of `IAM_AREAS`, or any string a deployment defines.
115
+ *
116
+ * Purely presentational: it groups the permission in an administrator's screen and is never read
117
+ * when a request is authorized. It is a TAG rather than a name segment precisely so an existing
118
+ * permission can be grouped without renaming it, which would orphan every grant already made.
119
+ */
120
+ area?: string;
121
+ /**
122
+ * Marks a permission the PLATFORM owns rather than one the application declared for itself.
123
+ *
124
+ * The distinction is not cosmetic: an operator must not be able to revoke a platform marker from
125
+ * an undifferentiated list, and a repair that deletes "definitions no declaration names" must not
126
+ * reach one — a marker can be enforced by a runtime decoration rather than by a gate parameter,
127
+ * so its absence from the declarations is not evidence that it is unused.
128
+ */
129
+ managed?: boolean;
36
130
  }
37
131
  export interface IamResourceSpec {
38
132
  name: string;
@@ -40,16 +134,40 @@ export interface IamResourceSpec {
40
134
  }
41
135
  /** A permission declared for an entity's client (project). */
42
136
  export interface IamPermissionDefinition {
43
- /** Canonical name: "res--action" or bare "res" when unscoped. */
137
+ /** Canonical name: "res--action" or bare "res" when unscoped. NEVER carries an `@` selector. */
44
138
  name: string;
45
139
  resource: string;
46
140
  action?: string;
47
141
  resourceScoped?: boolean;
48
142
  title?: string;
143
+ /** Grouping tag; presentational only. See `IamPermissionArgs.area`. */
144
+ area?: string;
145
+ /** Owned by the platform rather than declared by the application. See `IamPermissionArgs.managed`. */
146
+ managed?: boolean;
147
+ }
148
+ /** Narrows a definition listing. An `areas` entry of `null` matches definitions carrying no tag. */
149
+ export interface IamPermissionFilter {
150
+ areas?: (string | null)[];
151
+ managed?: boolean;
152
+ resourceScoped?: boolean;
49
153
  }
50
154
  export interface IamGrantArgs {
51
155
  /** Resource ids for the resource-scoped grant form; omit for an unscoped (project-wide) grant. */
52
156
  resources?: string[];
157
+ /**
158
+ * Which form to address. THE DEFAULTS ARE ASYMMETRIC ON PURPOSE, to preserve the semantics every
159
+ * existing caller already relies on:
160
+ *
161
+ * grantPermission — `resources != null` ? Resources : Blanket
162
+ * revokePermission — `resources != null` ? Resources : All
163
+ *
164
+ * Revoke defaults to `All` because a bare `revokePermission(...)` has always meant "remove it
165
+ * everywhere". Narrowing that to `Blanket` would leave the resource-scoped set standing for every
166
+ * existing caller — a live grant surviving a revoke, which is the failure direction nobody detects.
167
+ *
168
+ * Pass `mode` explicitly rather than depending on the default.
169
+ */
170
+ mode?: IamGrantMode;
53
171
  }
54
172
  /** A permission granted to an end-user subject of an entity's client. */
55
173
  export interface IamGrant {
@@ -60,6 +178,51 @@ export interface IamGrant {
60
178
  permission: string;
61
179
  /** Present only for resource-scoped grants. */
62
180
  resources?: string[];
181
+ /** Which form this record is. Derived from `resources`, not new information. */
182
+ mode?: IamGrantMode;
183
+ }
184
+ /** Grants every definition a filter selects, plus any named outright. */
185
+ export interface IamGrantBundle {
186
+ filter?: IamPermissionFilter;
187
+ /** Extra names included regardless of the filter — a platform marker, for instance. */
188
+ permissions?: string[];
189
+ /** Defaults to `Blanket`: a tier of access is a role, not a resource ACL. */
190
+ mode?: IamGrantMode;
191
+ /** Only meaningful with `mode: Resources`. */
192
+ resources?: string[];
193
+ }
194
+ export interface IamPermissionDeleteArgs {
195
+ /** Defaults to `Cascade`. */
196
+ policy?: IamRemovalPolicy;
197
+ /** Required to delete a `managed` definition. Defaults to false. */
198
+ managed?: boolean;
199
+ }
200
+ export interface IamPermissionRemoval {
201
+ permission: string;
202
+ clientId: string;
203
+ /** The definition existed and was removed. False is a normal, non-error outcome. */
204
+ found: boolean;
205
+ /** Grants stripped as part of the removal. */
206
+ revoked: IamGrant[];
207
+ }
208
+ export interface IamNormalizeArgs {
209
+ /** Compute and report the plan without writing anything. */
210
+ dryRun?: boolean;
211
+ }
212
+ export interface IamNormalizationReport {
213
+ clientId: string;
214
+ renamed: {
215
+ from: string;
216
+ to: string;
217
+ grantsMoved: number;
218
+ }[];
219
+ merged: {
220
+ from: string;
221
+ into: string;
222
+ grantsMoved: number;
223
+ }[];
224
+ /** Well-formed definitions deliberately left alone. */
225
+ untouched: string[];
63
226
  }
64
227
  /** An end-user of an entity (customer-wide; shared across that entity's projects). */
65
228
  export interface IamUser {
@@ -104,19 +267,87 @@ export interface IamService extends InitializedService {
104
267
  * misconfiguration is loud instead of yielding a silently wrong issuer.
105
268
  */
106
269
  getIssuerUrl: (entityId: string) => Promise<string>;
270
+ /**
271
+ * Find-or-create the entity's OIDC client.
272
+ *
273
+ * A client id is globally unique — a provider resolves it from the bare `client_id` a relying
274
+ * party sends, with no tenant context — so an implementation MUST refuse an existing record
275
+ * that belongs to a different entity (`IamClientError('client:entity-mismatch')`) rather than
276
+ * returning it. Silently adopting one hands the caller another tenant's secret and overwrites
277
+ * that tenant's redirect URIs.
278
+ */
107
279
  ensureClient: (entityId: string, clientId: string, options?: IamClientOptions) => Promise<IamClient>;
280
+ /**
281
+ * Reserve a client id for the entity without provisioning it, so a caller can find a free name
282
+ * before committing to it. Returns false when the id is already taken — by this entity or any
283
+ * other — and true when the reservation is now held.
284
+ *
285
+ * This exists because the id is minted from a name that may later be released (a project alias
286
+ * moves when its hostname is refused), so the naming index cannot answer whether an id is free.
287
+ * The registry is the only authority.
288
+ */
289
+ claimClient: (entityId: string, clientId: string) => Promise<boolean>;
290
+ /**
291
+ * Release a client id and everything keyed by it. Called when a project or slot is deleted —
292
+ * without it a recreated project can inherit a stale registration.
293
+ */
294
+ deleteClient: (entityId: string, clientId: string) => Promise<void>;
108
295
  /**
109
296
  * Ensures a permission/resource exists in the entity's client.
110
297
  * Returns the canonical resource name (e.g. "res--action" or "res").
298
+ *
299
+ * MERGES into an existing definition rather than replacing it: `resource` and `action` are
300
+ * re-derived and overwrite, while `title`, `resourceScoped`, `area` and `managed` are set when
301
+ * provided — including when provided as `false` — and KEPT when omitted. Replacing wholesale made
302
+ * every flag last-write-wins, so a caller that happened not to pass one erased it.
303
+ *
304
+ * It NEVER rewrites the name. Callers round-trip a definition's `resource` + `action` back through
305
+ * this method to copy a definition set between clients, so a rename here would silently orphan
306
+ * every grant made against the old name.
307
+ *
308
+ * The `resource` argument must not carry a gate selector: `@` is the gate's syntax and is never
309
+ * part of a stored name. Implementations reject one rather than storing a key no gate looks up.
111
310
  */
112
311
  ensurePermission: (entityId: string, clientId: string, resource?: string, args?: IamPermissionArgs) => Promise<string>;
312
+ /**
313
+ * Removes permission definitions, and by default every grant that referenced them.
314
+ *
315
+ * Takes NAMES: the definition record is keyed by name, and a malformed definition's stored
316
+ * `resource`/`action` cannot be trusted to recompose that key. Accepts an array so a repair costs
317
+ * one sweep of the entity's subjects rather than one per name.
318
+ *
319
+ * Idempotent — a name with no definition yields `{ found: false }` and writes nothing. Every other
320
+ * lifecycle operation here converges rather than erroring, and two repairs may run concurrently on
321
+ * one project.
322
+ *
323
+ * Implementations MUST revoke the grants before dropping the definition. There is no transaction
324
+ * across the two stores, and a crash between them must leave a definition with no grants — visible
325
+ * and re-runnable — rather than grants with no definition, which are invisible to any screen that
326
+ * renders the definition list yet still satisfy a gate.
327
+ *
328
+ * A `managed` definition is refused unless `args.managed` is explicitly true.
329
+ */
330
+ deletePermission: (entityId: string, clientId: string, permission: string | string[], args?: IamPermissionDeleteArgs) => Promise<IamPermissionRemoval[]>;
331
+ /**
332
+ * Repairs definitions whose stored name carries a gate selector, moving grants with them.
333
+ *
334
+ * Strictly a rename and merge — never a delete, never an invention. Where the stripped name
335
+ * already exists the two are merged, because one entrypoint scoping a permission by `:id` and
336
+ * another by `:enquiryId` describe ONE permission. The target is marked resource-scoped: a leaked
337
+ * selector is positive evidence that it is, and that evidence is otherwise discarded.
338
+ *
339
+ * Grants move in the same write that renames the definition, which is why this exists as its own
340
+ * operation: reconstructing it as delete + re-declare + re-grant drops real access on any failure
341
+ * between the three steps.
342
+ */
343
+ normalizePermissions: (entityId: string, clientId: string, args?: IamNormalizeArgs) => Promise<IamNormalizationReport>;
113
344
  /**
114
345
  * Idempotent: creates a resource, a client role for it, and assigns that role to
115
346
  * the entity-owner role. Used by payment provisioning for account/project/wl resources.
116
347
  */
117
348
  ensureResourceOwnership: (entityId: string, clientId: string, resource: IamResourceSpec) => Promise<void>;
118
- /** Lists permission definitions registered for the entity's client. */
119
- listPermissions: (entityId: string, clientId: string) => Promise<IamPermissionDefinition[]>;
349
+ /** Lists permission definitions registered for the entity's client, optionally narrowed. */
350
+ listPermissions: (entityId: string, clientId: string, filter?: IamPermissionFilter) => Promise<IamPermissionDefinition[]>;
120
351
  /**
121
352
  * Grants a permission to an end-user subject. With args.resources the grant is
122
353
  * resource-scoped (bound to those resource ids); without it the grant is project-wide.
@@ -127,11 +358,25 @@ export interface IamService extends InitializedService {
127
358
  * without it the whole grant is removed.
128
359
  */
129
360
  revokePermission: (entityId: string, clientId: string, profileId: string, permission: string, args?: IamGrantArgs) => Promise<void>;
361
+ /**
362
+ * Grants every permission a bundle selects to each subject, idempotently.
363
+ *
364
+ * Exists as its own operation rather than a loop over `grantPermission` because a backend that
365
+ * loads-mutates-saves a subject per call turns N permissions across M subjects into N·M writes and
366
+ * N·M lost-update windows on the same records. Collapsing to one read and one write per subject is
367
+ * also what makes re-running it genuinely idempotent.
368
+ *
369
+ * Returns what each subject now holds, not what changed.
370
+ */
371
+ grantBundle: (entityId: string, clientId: string, profileIds: string[], bundle: IamGrantBundle) => Promise<IamGrant[]>;
130
372
  /** Lists grants for the entity's client, optionally for a single subject. */
131
373
  listGrants: (entityId: string, clientId: string, profileId?: string) => Promise<IamGrant[]>;
132
374
  /**
133
- * Lists the entity's end-users. With clientId, narrows to users holding at least
134
- * one grant for that client (per-project view).
375
+ * Lists the entity's end-users. End-users are customer-wide — shared across every project of
376
+ * the entity — so `clientId` scopes the reported `grantCount` to one project's client and does
377
+ * **not** filter the set: a user who has authenticated against a project but holds no grant
378
+ * there is still that project's user, and the screen that manages grants is exactly where they
379
+ * must be visible.
135
380
  */
136
381
  listUsers: (entityId: string, clientId?: string) => Promise<IamUser[]>;
137
382
  /** Loads a single end-user by subject id, or null when absent. */
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@owlmeans/iam",
3
- "version": "0.1.18-rc.2",
3
+ "version": "0.1.18-rc.21",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "scripts": {
@@ -22,14 +22,15 @@
22
22
  }
23
23
  },
24
24
  "dependencies": {
25
- "@owlmeans/auth": "^0.1.18-rc.2",
26
- "@owlmeans/context": "^0.1.18-rc.2",
27
- "@owlmeans/error": "^0.1.18-rc.2",
28
- "@owlmeans/oidc": "^0.1.18-rc.2"
25
+ "@owlmeans/auth": "^0.1.18-rc.14",
26
+ "@owlmeans/context": "^0.1.18-rc.13",
27
+ "@owlmeans/error": "^0.1.18-rc.13",
28
+ "@owlmeans/oidc": "^0.1.18-rc.26",
29
+ "@owlmeans/route": "^0.1.18-rc.14"
29
30
  },
30
31
  "devDependencies": {
31
32
  "@owlmeans/dep-config": "workspace:*",
32
- "@types/bun": "^1.3.14",
33
+ "@types/bun": "^1.4.0",
33
34
  "nodemon": "^3.1.14",
34
35
  "typescript": "^7.0.2"
35
36
  },
package/src/consts.ts CHANGED
@@ -4,3 +4,108 @@ export const IAM_MODE_KEYCLOAK = 'keycloak'
4
4
  export const IAM_MODE_INTEGRATED = 'integrated'
5
5
 
6
6
  export type IamMode = typeof IAM_MODE_KEYCLOAK | typeof IAM_MODE_INTEGRATED
7
+
8
+ /**
9
+ * Gate-param syntax: `<permission>[@<selector>]`.
10
+ *
11
+ * The `@` belongs to the GATE and to nothing else. It says where a resource id is read from at
12
+ * request time; it is never part of a permission's stored NAME. A definition or a grant whose name
13
+ * carries one is a key nothing ever looks up, so every grant against it is a silent no-op.
14
+ */
15
+ export const RESOURCE_PARAM_SEPARATOR = '@'
16
+
17
+ /**
18
+ * Separates an explicit source from its path: `@body:order.id`.
19
+ *
20
+ * Deliberately `:` rather than a dot. Under a dotted-only form the meaning of a selector changes the
21
+ * moment a route gains a param named like a source keyword — `/report/:query` with `@query.id` flips
22
+ * from "the route param literally named `query.id`" to "the query string's `id`", redirecting an
23
+ * authorization lookup from a router-matched value to attacker-supplied input, caused by an
24
+ * unrelated route rename. `:` is decidable in one character, and no gate-param string written before
25
+ * this existed can contain one.
26
+ */
27
+ export const RESOURCE_SOURCE_SEPARATOR = ':'
28
+
29
+ /** Separates path segments inside a qualified selector: `@body:order.id`. */
30
+ export const RESOURCE_PATH_SEPARATOR = '.'
31
+
32
+ /** Where a qualified selector may read a resource id from. */
33
+ export enum GateParamSource {
34
+ Params = 'params',
35
+ Query = 'query',
36
+ Body = 'body',
37
+ Headers = 'headers',
38
+ Auth = 'auth'
39
+ }
40
+
41
+ /**
42
+ * What the bare form (`@enquiryId`) searches, in order.
43
+ *
44
+ * Params first, then query. The query fallback is legacy and deliberately kept: removing it would
45
+ * silently deny already-deployed applications whose selector happens to name a query key, and a
46
+ * silent denial inside a customer's running app is the worst available outcome.
47
+ */
48
+ export const DEFAULT_GATE_PARAM_SOURCES: GateParamSource[] = [
49
+ GateParamSource.Params, GateParamSource.Query
50
+ ]
51
+
52
+ /** Why a gate param is structurally wrong — a misconfiguration, not a failed authorization. */
53
+ export enum GateParamErrorCode {
54
+ UnknownSource = 'gate-param:unknown-source',
55
+ EmptySelector = 'gate-param:empty-selector',
56
+ EmptySegment = 'gate-param:empty-segment',
57
+ /** Named a request key the entrypoint's filter does not declare — AJV strips it before the gate. */
58
+ UnreachableKey = 'gate-param:unreachable-key',
59
+ /** Named a route param the entrypoint's own path does not carry. */
60
+ UnknownRouteParam = 'gate-param:unknown-route-param'
61
+ }
62
+
63
+ /** Why a selector produced no resource id. */
64
+ export enum GateResolutionFailure {
65
+ /** The source exists, the path does not. */
66
+ NotProvided = 'not-provided',
67
+ /** The request carries no such source at all. */
68
+ SourceMissing = 'source-missing',
69
+ /** Resolved to an object or a multi-element array — refused rather than stringified. */
70
+ NotScalar = 'not-scalar',
71
+ /** The selector itself did not parse. */
72
+ Malformed = 'malformed'
73
+ }
74
+
75
+ /**
76
+ * Which FORM of a grant an operation addresses.
77
+ *
78
+ * A resource-scoped permission is grantable two ways, under one name: `Blanket` (a PermissionSet
79
+ * carrying no `resources`, which `hasPermission` reads as covering every id) and `Resources` (an
80
+ * explicit id list). They are stored separately and must be revoked separately, or a UI offering
81
+ * both controls would have one silently wipe the other.
82
+ */
83
+ export enum IamGrantMode {
84
+ Blanket = 'blanket',
85
+ Resources = 'resources',
86
+ /** Revoke-only: every form. Illegal on a grant. */
87
+ All = 'all'
88
+ }
89
+
90
+ /** What to do about grants that still reference a definition being removed. */
91
+ export enum IamRemovalPolicy {
92
+ /** Strip the permission from every subject of the entity, then drop the definition. */
93
+ Cascade = 'cascade',
94
+ /** Refuse while any subject still holds it; the holders travel in the error. */
95
+ Refuse = 'refuse'
96
+ }
97
+
98
+ /**
99
+ * The grouping tiers a permission definition may be tagged with.
100
+ *
101
+ * A tag decides NOTHING at request time — `hasPermission` and every gate treat a permission name as
102
+ * one opaque string — and only groups the permission in an administrator's screen. `guest` is
103
+ * deliberately absent: a public surface declares no permissions, and adding an area later would
104
+ * re-classify names already tagged.
105
+ */
106
+ export const IAM_AREAS = ['user', 'operator', 'admin'] as const
107
+
108
+ export type IamArea = typeof IAM_AREAS[number]
109
+
110
+ /** Separates the resource from the action in a permission name. TWO hyphens, never one. */
111
+ export const PERMISSION_ACTION_SEPARATOR = '--'
package/src/errors.ts CHANGED
@@ -36,6 +36,24 @@ export class IamGrantError extends IamError {
36
36
  }
37
37
  }
38
38
 
39
+ /**
40
+ * A refusal about a permission DEFINITION, as opposed to a grant.
41
+ *
42
+ * Separate from `IamGrantError`, which means a grant's subject is missing or its entity does not
43
+ * match — reusing it for a definition-level refusal is a category error the next reader has to unpick.
44
+ *
45
+ * Codes: `permission:held:<name>` (still granted to someone, under a policy that refuses),
46
+ * `permission:managed:<name>` (platform-owned, deleted only with an explicit second key).
47
+ */
48
+ export class IamPermissionError extends IamError {
49
+ public static override typeName = `IamPermission${ResilientError.typeName}`
50
+
51
+ constructor(message = 'error') {
52
+ super(`permission:${message}`)
53
+ this.type = IamPermissionError.typeName
54
+ }
55
+ }
56
+
39
57
  export class IamUnsupported extends IamError {
40
58
  public static override typeName = `IamUnsupported${ResilientError.typeName}`
41
59
 
@@ -58,5 +76,6 @@ ResilientError.registerErrorClass(IamError)
58
76
  ResilientError.registerErrorClass(IamClientError)
59
77
  ResilientError.registerErrorClass(IamResourceError)
60
78
  ResilientError.registerErrorClass(IamGrantError)
79
+ ResilientError.registerErrorClass(IamPermissionError)
61
80
  ResilientError.registerErrorClass(IamUnsupported)
62
81
  ResilientError.registerErrorClass(IamUserError)
@@ -0,0 +1,3 @@
1
+ export * from './param.js'
2
+ export * from './resolve.js'
3
+ export * from './validate.js'
@@ -0,0 +1,122 @@
1
+ import {
2
+ RESOURCE_PARAM_SEPARATOR, RESOURCE_SOURCE_SEPARATOR, RESOURCE_PATH_SEPARATOR,
3
+ GateParamSource, GateParamErrorCode, DEFAULT_GATE_PARAM_SOURCES
4
+ } from '../consts.js'
5
+ import type { GateResourceSelector, GateParamProblem, ParsedGateParam } from '../types.js'
6
+
7
+ const SOURCES = Object.values(GateParamSource) as string[]
8
+
9
+ /**
10
+ * Read the `@…` half of a gate param.
11
+ *
12
+ * Two forms, and the difference between them is deliberate:
13
+ *
14
+ * `@enquiryId` — bare. A FLAT key, searched in `params` then `query`. It is never split on
15
+ * a dot, because a query key legally contains one and splitting it would
16
+ * change the meaning of selectors already deployed.
17
+ * `@body:order.id` — qualified. An explicit source and a nested path.
18
+ *
19
+ * The rule worth remembering: the bare form is lenient and flat, the qualified form is strict and
20
+ * nested.
21
+ */
22
+ export const parseGateSelector = (selector: string): GateResourceSelector | GateParamProblem => {
23
+ if (selector === '') {
24
+ return { code: GateParamErrorCode.EmptySelector, detail: 'the selector after "@" is empty' }
25
+ }
26
+
27
+ const idx = selector.indexOf(RESOURCE_SOURCE_SEPARATOR)
28
+ if (idx < 0) {
29
+ return {
30
+ selector,
31
+ path: [selector],
32
+ sources: DEFAULT_GATE_PARAM_SOURCES
33
+ }
34
+ }
35
+
36
+ const source = selector.slice(0, idx)
37
+ const rest = selector.slice(idx + 1)
38
+
39
+ if (!SOURCES.includes(source)) {
40
+ return {
41
+ code: GateParamErrorCode.UnknownSource,
42
+ detail: `"${source}" is not one of ${SOURCES.join(', ')}`
43
+ }
44
+ }
45
+
46
+ if (rest === '') {
47
+ return {
48
+ code: GateParamErrorCode.EmptySelector,
49
+ detail: `"${source}:" names no path`
50
+ }
51
+ }
52
+
53
+ const path = rest.split(RESOURCE_PATH_SEPARATOR)
54
+ if (path.some(segment => segment === '')) {
55
+ return {
56
+ code: GateParamErrorCode.EmptySegment,
57
+ detail: `"${rest}" has an empty path segment`
58
+ }
59
+ }
60
+
61
+ return {
62
+ selector,
63
+ source: source as GateParamSource,
64
+ path,
65
+ sources: [source as GateParamSource]
66
+ }
67
+ }
68
+
69
+ /**
70
+ * Split a gate param into the permission it checks and where the resource id comes from.
71
+ *
72
+ * Splits at the FIRST `@`. Everything before it is the permission name looked up in the subject's
73
+ * grants; everything after says where to read the resource id at request time. The suffix is the
74
+ * gate's syntax and nothing else's — a permission stored or granted under a name containing one is a
75
+ * key nothing ever looks up.
76
+ */
77
+ export const parseGateParam = (param: string): ParsedGateParam => {
78
+ const idx = param.indexOf(RESOURCE_PARAM_SEPARATOR)
79
+ if (idx < 0) {
80
+ return { permission: param }
81
+ }
82
+
83
+ const permission = param.slice(0, idx)
84
+ const selector = param.slice(idx + 1)
85
+ const parsed = parseGateSelector(selector)
86
+
87
+ if ('code' in parsed) {
88
+ return { permission, resourceParam: selector, error: parsed }
89
+ }
90
+
91
+ return {
92
+ permission,
93
+ // Kept for readers written against the flat form. Only meaningful when the form IS flat.
94
+ ...(parsed.source == null ? { resourceParam: selector } : {}),
95
+ resource: parsed
96
+ }
97
+ }
98
+
99
+ /** Compose a gate param. The inverse of `parseGateParam` for every selector it accepts. */
100
+ export const formatGateParam = (
101
+ permission: string,
102
+ selector?: string | { source?: GateParamSource, path: string[] }
103
+ ): string => {
104
+ if (selector == null) {
105
+ return permission
106
+ }
107
+
108
+ if (typeof selector === 'string') {
109
+ return selector === '' ? permission : `${permission}${RESOURCE_PARAM_SEPARATOR}${selector}`
110
+ }
111
+
112
+ if (selector.path.length < 1) {
113
+ return permission
114
+ }
115
+
116
+ const path = selector.path.join(RESOURCE_PATH_SEPARATOR)
117
+ const tail = selector.source == null
118
+ ? path
119
+ : `${selector.source}${RESOURCE_SOURCE_SEPARATOR}${path}`
120
+
121
+ return `${permission}${RESOURCE_PARAM_SEPARATOR}${tail}`
122
+ }