@owlmeans/iam 0.1.18-rc.50 → 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 +185 -17
- package/agent-meta/manifest.json +2 -2
- package/agent-meta/skills/iam/SKILL.md +1 -1
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -1,34 +1,202 @@
|
|
|
1
1
|
# @owlmeans/iam
|
|
2
2
|
|
|
3
|
-
Provider-agnostic IAM
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
-
|
|
11
|
-
|
|
12
|
-
|
|
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.
|
|
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
|
-
//
|
|
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
|
-
|
|
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.
|
|
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
|
package/agent-meta/manifest.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 2,
|
|
3
3
|
"package": "@owlmeans/iam",
|
|
4
|
-
"version": "0.1.18-rc.
|
|
5
|
-
"generatedAt": "2026-10-
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
29
|
+
"@owlmeans/oidc": "^0.1.18-rc.57",
|
|
30
30
|
"@owlmeans/route": "^0.1.18-rc.35"
|
|
31
31
|
},
|
|
32
32
|
"peerDependencies": {
|