@owlmeans/iam 0.1.18-rc.7 → 0.1.18-rc.9

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.
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "schemaVersion": 2,
3
3
  "package": "@owlmeans/iam",
4
- "version": "0.1.18-rc.6",
5
- "generatedAt": "2026-08-18T14:48:00.513Z",
4
+ "version": "0.1.18-rc.8",
5
+ "generatedAt": "2026-08-24T21:14:00.118Z",
6
6
  "canonicalRepo": "https://github.com/owlmeans/common",
7
7
  "entries": [
8
8
  {
@@ -23,6 +23,9 @@ Provider-agnostic IAM abstraction. Defines `IamService` interface and related ty
23
23
  | `IamPermissionDefinition` | type | Declared permission `{ name, resource, action?, resourceScoped?, title? }` |
24
24
  | `IamGrantArgs` | type | `{ resources?: string[] }` — present = resource-scoped grant form |
25
25
  | `IamGrant` | type | `{ profileId, clientId, permission, resources? }` |
26
+ | `IamUser` | type | End-user of an entity `{ profileId, email?, name?, role, disabled?, grantCount? }` |
27
+ | `IamUserInvite` | type | `{ email, name?, role? }` — find-or-create args |
28
+ | `IamUserUpdate` | type | `{ name?, role?, disabled? }` |
26
29
  | `hasPermission` | fn | `(auth, permission, { scope?, resourceId? }?)` — checks `Authorization.permissions` PermissionSet[]; an unscoped set satisfies a resourceId check |
27
30
  | `DEFAULT_ALIAS` | const | Default service alias `'iam-service'` |
28
31
  | `IAM_MODE_KEYCLOAK` | const | `'keycloak'` |
@@ -62,8 +65,15 @@ interface IamService extends InitializedService {
62
65
 
63
66
  // Provision an OIDC client for a tenant and return its credentials.
64
67
  // options.redirectUris hardens the client; always pass them (see "Issuer & redirect URIs").
68
+ // MUST refuse an existing record owned by another entity — see "A client id is a global name".
65
69
  ensureClient: (entityId: string, clientId: string, options?: IamClientOptions) => Promise<IamClient>
66
70
 
71
+ // Reserve a free client id without provisioning it; false when anyone else holds it.
72
+ claimClient: (entityId: string, clientId: string) => Promise<boolean>
73
+
74
+ // Release a client id and everything keyed by it (called when a project/slot is deleted).
75
+ deleteClient: (entityId: string, clientId: string) => Promise<void>
76
+
67
77
  // Provision a named permission on a client resource; returns the resource name
68
78
  ensurePermission: (entityId: string, clientId: string, resource?: string, args?: IamPermissionArgs) => Promise<string>
69
79
 
@@ -83,9 +93,49 @@ interface IamService extends InitializedService {
83
93
 
84
94
  // List grants for the client, optionally for one subject
85
95
  listGrants: (entityId: string, clientId: string, profileId?: string) => Promise<IamGrant[]>
96
+
97
+ // --- End-user management (customer-wide users, shared per entityId) ---
98
+
99
+ // Every end-user of the entity. clientId scopes the reported grantCount — it does NOT filter.
100
+ listUsers: (entityId: string, clientId?: string) => Promise<IamUser[]>
101
+ getUser: (entityId: string, profileId: string) => Promise<IamUser | null>
102
+ // Find-or-create by email — the one way an end-user record comes into existence.
103
+ inviteUser: (entityId: string, invite: IamUserInvite) => Promise<IamUser>
104
+ updateUser: (entityId: string, profileId: string, update: IamUserUpdate) => Promise<IamUser>
105
+ removeUser: (entityId: string, profileId: string) => Promise<void>
86
106
  }
87
107
  ```
88
108
 
109
+ ## End-users are customer-wide
110
+
111
+ An end-user belongs to the **entity**, not to a project: one customer's users are shared across every
112
+ project they own. `listUsers` therefore returns the entity's whole set and `clientId` only scopes the
113
+ reported `grantCount` to one project's client. Do not reintroduce a per-client filter — a person who
114
+ has authenticated against a project but holds no grant there is still that project's user, and the
115
+ screen that hands out grants is exactly where they have to be visible.
116
+
117
+ `inviteUser` is idempotent by email, which is what lets an invitation and a first sign-in converge on
118
+ one record instead of accumulating twins. Backends that have no user store of their own (keycloak,
119
+ where the customer manages realm users in its console) throw `IamUnsupported('user-management')` from
120
+ all five methods; callers turn that into an "external console" fallback rather than an error.
121
+
122
+ ## A client id is a global name
123
+
124
+ A provider resolves a client from the bare `client_id` a relying party sends — the adapter gets no
125
+ tenant context — so the id is unique across the whole deployment, not per entity, and the store
126
+ cannot namespace it on a consumer's behalf. Uniqueness has to be *in the id*.
127
+
128
+ - Compose it with the owning entity in it. A name derived from a project's own name alone lets two
129
+ organizations share one registration: one secret, one redirect-URI list, one permission-definition
130
+ set, and one grant namespace, because `PermissionSet.scope` is that same string.
131
+ - `ensureClient` refuses a record belonging to another entity rather than returning it. The failure
132
+ is deliberate — a loud provisioning error is the only alternative to a silent cross-tenant handover.
133
+ - Assign an id **once** and reserve it with `claimClient`. Whatever a consumer derives ids from
134
+ (a project alias, a slug) may later be released and re-used by a sibling, so only the registry can
135
+ say whether an id is free. `deleteClient` gives it back, or a deleted project's name is burned.
136
+ - Backends whose clients are already per-tenant (keycloak realms) satisfy this by construction:
137
+ `claimClient` returns true and `deleteClient` may be `IamUnsupported`.
138
+
89
139
  ## `IamClient.realm` field
90
140
 
91
141
  `realm` is set by the provider to the entity ID. Use it instead of accessing internal properties:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@owlmeans/iam",
3
- "version": "0.1.18-rc.7",
3
+ "version": "0.1.18-rc.9",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "scripts": {
@@ -25,7 +25,7 @@
25
25
  "@owlmeans/auth": "^0.1.18-rc.6",
26
26
  "@owlmeans/context": "^0.1.18-rc.6",
27
27
  "@owlmeans/error": "^0.1.18-rc.6",
28
- "@owlmeans/oidc": "^0.1.18-rc.6"
28
+ "@owlmeans/oidc": "^0.1.18-rc.8"
29
29
  },
30
30
  "devDependencies": {
31
31
  "@owlmeans/dep-config": "workspace:*",