@owlmeans/iam 0.1.18-rc.51 → 0.1.18-rc.52

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 CHANGED
@@ -1,34 +1,202 @@
1
1
  # @owlmeans/iam
2
2
 
3
- Provider-agnostic IAM abstraction for the OwlMeans framework — `IamService` interface, client/realm types, and permission helpers.
4
-
5
- ## Overview
6
-
7
- - `IamService` — unified interface for provisioning clients, realms, and permissions across IAM backends (e.g. Keycloak, built-in)
8
- - `IamClient` / `IamCredentialsPair` / `IamResourceSpec` — shared contract types used by server-side IAM implementations
9
- - `IamPermissionArgs` — describes a named permission: resource and organization binding (`resourceScoped`, `entityScoped`), title, area and `defaultClass`
10
- - Tenancy facets on `IamService` — `organizations`, `groups`, `subjects` — and `unsupportedFacet` for a backend without them
11
- - `makeIamRuntimeProtocols` — declarations of the provider's runtime IAM API (organizations, members, permissions, grants)
12
- - `hasPermission` — utility to check whether a set of grants covers a given permission, resource and acting organization
13
- - Errors: `IamError` and subtypes for client, permission, user, organization and group failures
3
+ Provider-agnostic IAM contracts: the `IamService` interface (client provisioning, permission
4
+ definitions and grants, users of a client, organization/group/subject facets), the permission-name
5
+ and gate-param grammars, `hasPermission`, the IAM errors, and the protocol declarations of the
6
+ provider's runtime IAM API. An app imports it to type the IAM service it resolves by alias, to
7
+ declare and grant permissions, to check an `Authorization` against a permission, and to build gate
8
+ params. It contains no implementation and talks to no provider — a concrete backend (a Keycloak
9
+ proxy, or the integrated backend built on `@owlmeans/server-oidc-provider`) ships separately, the
10
+ server-side gate and runtime client are `@owlmeans/server-iam`, and the browser half (organization
11
+ switch, `hasPermission` re-export) is `@owlmeans/client-iam`. Permission decisions belong on the
12
+ server; the `iam` skill holds the full model.
14
13
 
15
14
  ## Installation
16
15
 
17
16
  ```bash
18
- bun add @owlmeans/iam@^0.1.18-rc.51
17
+ bun add @owlmeans/iam@^0.1.18-rc.52
19
18
  ```
20
19
 
20
+ `ajv` is a peer dependency (the runtime API schemas are `JSONSchemaType`).
21
+
22
+ ## Concepts
23
+
24
+ - **Owning organization entity vs. acting organization** — every `IamService` method takes
25
+ `entityId` first: the organization entity that **owns** the client, resolved server-side. The
26
+ organizations a client's subjects act in are named by `entitySlug` inside the facets and on the
27
+ wire.
28
+ - **Client** — an OIDC client (`IamClient`) of one organization entity. Its id is globally unique,
29
+ because a provider resolves the bare `client_id` with no tenant context.
30
+ - **Permission name** — `<resource>--<action>` with **two** hyphens (`PERMISSION_ACTION_SEPARATOR`);
31
+ a bare `<resource>` is unscoped. A name never carries `@`.
32
+ - **Permission kinds** — a definition is unbound, `entityScoped` (grants bound to one organization),
33
+ `resourceScoped` (grants may list resource ids), or both. The kind is a property of the definition.
34
+ - **Grant form** — `IamGrantMode.Blanket` (no `resources`, covers every id), `Resources` (an explicit
35
+ id list), `All` (revoke-only). The two grantable forms are stored separately.
36
+ - **Default class** — `IamDefaultClass.User` / `Member` / `Owner` / `None`: who holds a definition
37
+ without an explicit grant, evaluated when claims are built.
38
+ - **Facets** — `organizations`, `groups`, `subjects` on `IamService`; a backend without one answers
39
+ `unsupportedFacet(what)`.
40
+ - **Gate param** — `<permission>[@<selector>]` on an entrypoint guard; the selector says where the
41
+ resource id is read at request time: bare `@enquiryId` (flat key, params then query) or qualified
42
+ `@body:order.id` (one source, nested path).
43
+
21
44
  ## Usage
22
45
 
46
+ ### Check a permission
47
+
23
48
  ```typescript
24
- import type { IamService, IamClient } from '@owlmeans/iam'
25
49
  import { hasPermission } from '@owlmeans/iam'
26
50
 
27
- // Check whether an auth payload grants a permission for one record, in the organization the request acts in
28
- const allowed = hasPermission(auth, 'order--edit', { resourceId: 'ord-1', entitySlug: 'acme' })
51
+ // A blanket set satisfies a resourceId check; a bound set counts only for the named organization
52
+ const allowed = hasPermission(auth, 'order--edit', { scope: clientId, resourceId: 'ord-1', entitySlug: 'acme' })
29
53
  ```
30
54
 
31
- Typically consumed via `@owlmeans/server-iam` which wires the concrete OIDC-backed implementation.
55
+ ### Declare and grant permissions
56
+
57
+ ```typescript
58
+ import { DEFAULT_ALIAS, IamDefaultClass, IamGrantMode, permissionNameHelper } from '@owlmeans/iam'
59
+ import type { IamService } from '@owlmeans/iam'
60
+
61
+ const iam = context.service<IamService>(DEFAULT_ALIAS)
62
+
63
+ const name = await iam.ensurePermission(entityId, clientId, 'order', {
64
+ permission: 'edit', resourceScoped: true, entityScoped: true, area: 'user',
65
+ defaultClass: IamDefaultClass.Owner,
66
+ }) // 'order--edit'
67
+
68
+ await iam.grantPermission(entityId, clientId, profileId, name, {
69
+ mode: IamGrantMode.Resources, resources: ['ord-1'], entitySlug: 'acme',
70
+ })
71
+ await iam.revokePermission(entityId, clientId, profileId, name, { mode: IamGrantMode.All, entitySlug: 'acme' })
72
+
73
+ permissionNameHelper.parsePermissionName('order--edit') // { name, resource: 'order', action: 'edit' }
74
+ permissionNameHelper.composePermissionName({ resource: 'order', action: 'edit' })
75
+ ```
76
+
77
+ ### Provision a client
78
+
79
+ ```typescript
80
+ const client = await iam.ensureClient(entityId, clientId, {
81
+ redirectUris: ['https://acme.example.com/callback'],
82
+ config: { users: true }, // tenanted: subjects get organizations of their own
83
+ })
84
+ const issuer = await iam.getIssuerUrl(entityId) // pass through as the discovery URL, never rebuild it
85
+ ```
86
+
87
+ ### Work with gate params
88
+
89
+ ```typescript
90
+ import { GateParamSource, gateParamHelper, gateValidationHelper, resolveGateResource } from '@owlmeans/iam'
91
+
92
+ const param = gateParamHelper.formatGateParam('order--edit', { source: GateParamSource.Body, path: ['order', 'id'] })
93
+ // 'order--edit@body:order.id'
94
+
95
+ const { permission, resource, error } = gateParamHelper.parseGateParam(param)
96
+ const resolved = resource != null ? resolveGateResource(req, resource) : undefined // { id?, from?, reason? }
97
+
98
+ const issues = gateValidationHelper.validateGateParams(['order--view@orderId'], { routePath: '/orders/:orderId' })
99
+ ```
100
+
101
+ ### A backend without a facet
102
+
103
+ ```typescript
104
+ import { unsupportedFacet } from '@owlmeans/iam'
105
+ import type { IamGroupFacet } from '@owlmeans/iam'
106
+
107
+ const groups = unsupportedFacet<IamGroupFacet>('groups') // every method throws IamUnsupported('groups') synchronously
108
+ ```
109
+
110
+ ## API
111
+
112
+ ### `IamService` (alias `DEFAULT_ALIAS` = `'iam-service'`)
113
+
114
+ | Group | Methods |
115
+ |---|---|
116
+ | Admin config | `getEntityAdminConfig`, `getCredentialsPair`, `getIssuerUrl` |
117
+ | Clients | `ensureClient(entityId, clientId, { redirectUris?, config? }?)`, `getClient`, `claimClient`, `deleteClient` |
118
+ | Definitions | `ensurePermission`, `deletePermission(…, names, { policy?, managed? }?)`, `normalizePermissions`, `ensureResourceOwnership`, `listPermissions(…, filter?)` |
119
+ | Grants | `grantPermission`, `revokePermission`, `grantBundle`, `listGrants` |
120
+ | Users of a client | `listUsers`, `getUser`, `inviteUser`, `updateUser`, `removeUser`, `syncStaff` |
121
+ | Facets | `organizations` (`IamOrganizationFacet`), `groups` (`IamGroupFacet`), `subjects` (`IamSubjectFacet`) |
122
+
123
+ ### Functions and helpers
124
+
125
+ | Symbol | Purpose |
126
+ |---|---|
127
+ | `hasPermission(auth, permission, { scope?, resourceId?, entitySlug? }?)` | Does an `Authorization`'s permission sets grant it |
128
+ | `permissionNameHelper` (`createPermissionNameHelper()`) | `parsePermissionName`, `composePermissionName`, `isPermissionName` |
129
+ | `gateParamHelper` (`createGateParamHelper()`) | `parseGateParam`, `parseGateSelector`, `formatGateParam` |
130
+ | `gateValidationHelper` (`createGateValidationHelper()`) | `validateGateParams(params, audit?)`, `routeParamsOf(path)` |
131
+ | `resolveGateResource(req, selector)` | Read the resource id a selector points at → `{ id?, from?, reason? }` |
132
+ | `unsupportedFacet<T>(what)` | A facet whose every method throws `IamUnsupported(what)` |
133
+ | `makeIamRuntimeProtocols({ service, path? })` | Protocol tree of the runtime IAM API: `organizations.{list,create,update}`, `members.{list,add,update,remove}`, `permissions.list`, `grants.{list,assign,revoke}` |
134
+ | `parsePermissionName`, `isPermissionName`, `parseGateParam` | Deprecated standalone forms of the helper members |
135
+
136
+ ### Constants and enums
137
+
138
+ | Symbol | Value / members |
139
+ |---|---|
140
+ | `IAM_MODE_KEYCLOAK`, `IAM_MODE_INTEGRATED`, `IamMode` | `'keycloak'`, `'integrated'` — a config seam for the app's own service factory |
141
+ | `IAM_AREAS`, `IamArea` | `['user', 'operator', 'admin']` — presentational grouping tags |
142
+ | `PERMISSION_ACTION_SEPARATOR` | `'--'` |
143
+ | `RESOURCE_PARAM_SEPARATOR`, `RESOURCE_SOURCE_SEPARATOR`, `RESOURCE_PATH_SEPARATOR` | `'@'`, `':'`, `'.'` |
144
+ | `GateParamSource`, `DEFAULT_GATE_PARAM_SOURCES` | `Params` `Query` `Body` `Headers` `Auth`; bare form searches `[Params, Query]` |
145
+ | `GateParamErrorCode`, `GateResolutionFailure` | structural selector problems; why a selector yielded no id |
146
+ | `IamGrantMode`, `IamRemovalPolicy`, `IamDefaultClass`, `IamGrantOrigin` | `Blanket`/`Resources`/`All`; `Cascade`/`Refuse`; `None`/`User`/`Member`/`Owner`; `Direct`/`Default`/`Group` |
147
+ | `IAM_MEMBERS_GROUP`, `IAM_EMAIL_PROOF` | `'members'` (managed staff group), `'proof:email'` |
148
+ | `IAM_RUNTIME_GUARD`, `IAM_RUNTIME_PATH`, `IAM_RUNTIME_ROUTES` | runtime API guard alias, default base `'/runtime'`, leaf-path table |
149
+ | `IamRuntime*Schema` | closed AJV schemas of the runtime API's wire shapes |
150
+
151
+ ### Types
152
+
153
+ `IamClient`, `IamClientInfo`, `IamClientConfig`, `IamClientOptions`, `IamCredentialsPair`,
154
+ `IamPermissionArgs`, `IamPermissionDefinition`, `IamPermissionFilter`, `IamResourceSpec`,
155
+ `IamGrant`, `IamGrantArgs`, `IamGrantBundle`, `IamGroupRef`, `IamPermissionDeleteArgs`,
156
+ `IamPermissionRemoval`, `IamNormalizeArgs`, `IamNormalizationReport`, `IamUser`, `IamUserInvite`,
157
+ `IamUserUpdate`, `IamMembershipInfo`, `IamOrganization*`, `IamMember*`, `IamGroup*`,
158
+ `IamStaffSync*`, `IamSubject`, `IamSignIn*`, the three facet interfaces, `HasPermissionOptions`,
159
+ `ParsedPermissionName`, `PermissionNameHelper`, the `Gate*` types and the `IamRuntime*` wire types.
160
+
161
+ ### Errors
162
+
163
+ All extend `IamError` (a `ResilientError`) and are registered for marshalling.
164
+
165
+ | Class | Meaning |
166
+ |---|---|
167
+ | `IamClientError` | client invalid or unusable — `client:entity-mismatch`, `redirect-uris`, unconfigured provider route |
168
+ | `IamResourceError` | a provider resource came back without a name |
169
+ | `IamGrantError` | grant subject missing or entity mismatch |
170
+ | `IamPermissionError` | definition refusal — `permission:held:<name>`, `permission:managed:<name>`, a disallowed `defaultClass` |
171
+ | `IamUserError` | an end-user operation was refused |
172
+ | `IamOrganizationError` | unknown organization, last owner, membership cap |
173
+ | `IamGroupError` | unknown group key, managed group written by an operator, another client's group |
174
+ | `IamUnsupported` | the active backend cannot do it (`user-management`, `resource-scoped-grant`, a facet) |
175
+
176
+ ## Common pitfalls
177
+
178
+ - `revokePermission` without `mode` defaults to `All`, `grantPermission` to `Blanket`; pass `mode`
179
+ explicitly.
180
+ - Granting an `entityScoped` definition requires `args.entitySlug`; any other definition refuses it.
181
+ - One hyphen is not a separator: `enquiry-view` is a resource with no action. Build names with
182
+ `permissionNameHelper`, never by splitting strings.
183
+ - `@` belongs to the gate param, never to a stored name — `ensurePermission` refuses it.
184
+ - Always pass `redirectUris` to `ensureClient`; the integrated provider refuses the legacy wildcard.
185
+ - Never rebuild an issuer URL from a host and base path — use `getIssuerUrl`.
186
+ - A client id must include the owning organization entity and be reserved with `claimClient`;
187
+ `ensureClient` refuses another entity's record.
188
+ - Use `grantBundle` instead of looping over `grantPermission`.
189
+ - `getCredentialsPair` returns a raw admin token — never hand it to a request handler.
190
+ - `entityId` never comes from a token or request; the wire names organizations by `entitySlug` only.
191
+
192
+ ## Related packages
193
+
194
+ - [`@owlmeans/server-iam`](../server-iam) — the IAM gate, the session's organizations on the server, the runtime IAM client
195
+ - [`@owlmeans/client-iam`](../client-iam) — browser wiring, organization switch, `hasPermission` re-export
196
+ - [`@owlmeans/oidc`](../oidc) — provider descriptors, claim shapes, the `iamMode` config field
197
+ - [`@owlmeans/auth`](../auth) — `Authorization` and `PermissionSet`, which `hasPermission` reads
198
+ - [`@owlmeans/server-oidc-provider`](../server-oidc-provider) — the provider the integrated backend is built on
199
+ - [`@owlmeans/entrypoint`](../entrypoint) — the protocol builders behind `makeIamRuntimeProtocols`
32
200
 
33
201
  <!-- owlmeans:agent-guidance:start -->
34
202
  ## Agent guidance
@@ -38,7 +206,7 @@ This package ships embedded agent skills under `agent-meta/`. After installing y
38
206
  your project's skill store (`.agents/skills/`):
39
207
 
40
208
  ```sh
41
- npx @owlmeans/agent-skills@^0.1.18-rc.51
209
+ npx @owlmeans/agent-skills@^0.1.18-rc.52
42
210
  ```
43
211
 
44
212
  The embedded files are version-matched to this package release. Do not edit them
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "schemaVersion": 2,
3
3
  "package": "@owlmeans/iam",
4
- "version": "0.1.18-rc.51",
5
- "generatedAt": "2026-10-06T11:45:57.661Z",
4
+ "version": "0.1.18-rc.52",
5
+ "generatedAt": "2026-10-08T21:27:54.296Z",
6
6
  "canonicalRepo": "https://github.com/owlmeans/common",
7
7
  "entries": [
8
8
  {
@@ -8,7 +8,7 @@ metadata:
8
8
 
9
9
  # Using `@owlmeans/iam`
10
10
 
11
- **Install:** `"@owlmeans/iam": "^0.1.18-rc.51"` in `dependencies`
11
+ **Install:** `"@owlmeans/iam": "^0.1.18-rc.52"` in `dependencies`
12
12
 
13
13
  Provider-agnostic IAM abstraction: the `IamService` interface, the permission and grant shapes, the
14
14
  gate-param grammar, `hasPermission`, and the declarations of the provider's runtime IAM API. It
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@owlmeans/iam",
3
- "version": "0.1.18-rc.51",
3
+ "version": "0.1.18-rc.52",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "scripts": {
@@ -22,11 +22,11 @@
22
22
  }
23
23
  },
24
24
  "dependencies": {
25
- "@owlmeans/auth": "^0.1.18-rc.40",
25
+ "@owlmeans/auth": "^0.1.18-rc.41",
26
26
  "@owlmeans/context": "^0.1.18-rc.34",
27
27
  "@owlmeans/entrypoint": "^0.1.18-rc.41",
28
28
  "@owlmeans/error": "^0.1.18-rc.38",
29
- "@owlmeans/oidc": "^0.1.18-rc.56",
29
+ "@owlmeans/oidc": "^0.1.18-rc.57",
30
30
  "@owlmeans/route": "^0.1.18-rc.35"
31
31
  },
32
32
  "peerDependencies": {