@owlmeans/server-auth-identity 0.1.18-rc.1 → 0.1.18-rc.11

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
@@ -7,7 +7,7 @@ Mongo-backed local identity resources and provider account linking for OwlMeans
7
7
  - Registers three Mongo resources: local accounts, local profiles, and provider credentials.
8
8
  - Provides `IdentityLinkingService` to map external provider profile details into an OwlMeans `AuthPayload`.
9
9
  - Keeps external provider identity separate from local product identity and authorization scopes.
10
- - Designed to work with `@owlmeans/server-auth`, `@owlmeans/server-oidc-rp`, and product-specific module gates.
10
+ - Designed to work with `@owlmeans/server-auth`, `@owlmeans/server-oidc-rp`, and product-specific entrypoint gates.
11
11
 
12
12
  ## Installation
13
13
 
@@ -43,9 +43,10 @@ import { AUTH_IDENTITY_PROFILE } from '@owlmeans/server-auth-identity'
43
43
  import type { IdentityProfileResource } from '@owlmeans/server-auth-identity'
44
44
 
45
45
  const profiles = context.resource<IdentityProfileResource>(AUTH_IDENTITY_PROFILE)
46
- const profile = await profiles.load(profileId, 'profileId')
46
+ const profile = await profiles.load({ profileId })
47
+ const scoped = await profiles.load({ entityId, profileId })
47
48
 
48
- const { items } = await profiles.list({ entityId, profileId } as any)
49
+ const { items, total } = await profiles.list({ entityId })
49
50
  ```
50
51
 
51
52
  ## API
@@ -83,7 +84,7 @@ const { items } = await profiles.list({ entityId, profileId } as any)
83
84
  - Google/OIDC login is only the provider bootstrap path; durable authorization data lives in `IdentityProfile`.
84
85
  - Backend contexts should register `appendAuthService(context)`, then `appendAuthIdentityResources(context)`, then product gate services.
85
86
  - Product gates should read `AUTH_IDENTITY_PROFILE`, verify `entityId`, reject expired or blocked profiles, and compare gate params against profile scopes.
86
- - `Resource.pick()` is destructive and deletes the matching record. Never use `pick()` for gate or handler reads; use `load()` or `list()`.
87
+ - `Resource.take()` is destructive and deletes the record it returns. Never use `take()` for gate or handler reads; use `load(where)` or `list(where)`.
87
88
 
88
89
  ## Related Packages
89
90
 
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "schemaVersion": 2,
3
3
  "package": "@owlmeans/server-auth-identity",
4
- "version": "0.1.18-rc.0",
5
- "generatedAt": "2026-08-16T22:20:50.498Z",
4
+ "version": "0.1.18-rc.11",
5
+ "generatedAt": "2026-09-01T16:28:56.980Z",
6
6
  "canonicalRepo": "https://github.com/owlmeans/common",
7
7
  "entries": [
8
8
  {
@@ -8,7 +8,7 @@ user-invocable: false
8
8
  # @owlmeans/server-auth-identity
9
9
 
10
10
  **Layer:** Server
11
- **Install:** `"@owlmeans/server-auth-identity": "^0.1.18-rc.0"` in `dependencies`
11
+ **Install:** `"@owlmeans/server-auth-identity": "^0.1.18-rc.11"` in `dependencies`
12
12
 
13
13
  ## Key Exports
14
14
 
@@ -54,12 +54,12 @@ import type { IdentityProfileResource } from '@owlmeans/server-auth-identity'
54
54
 
55
55
  const profileRes = ctx.resource<IdentityProfileResource>(AUTH_IDENTITY_PROFILE)
56
56
 
57
- // Single-field indexed lookup (read-only):
58
- const profile = await profileRes.load(profileId, 'profileId')
57
+ // One record by any field, or by several at once — read-only, and typed by the resource:
58
+ const profile = await profileRes.load({ profileId })
59
+ const scoped = await profileRes.load({ entityId, profileId })
59
60
 
60
- // Multi-field criteria lookup (read-only):
61
- const { items } = await profileRes.list({ entityId, profileId } as any)
62
- const profile = items[0] ?? null
61
+ // Every match, newest first:
62
+ const { items, total } = await profileRes.list({ entityId }, { sort: [{ field: 'createdAt', order: 'desc' }] })
63
63
  ```
64
64
 
65
65
  ### Link an external provider to a local profile
@@ -120,7 +120,7 @@ by the reference conversion's design.
120
120
 
121
121
  ## Important Gotchas
122
122
 
123
- - **`Resource.pick()` is destructive** — it deletes the record it finds. For read-only identity lookups in gates or handlers, use `load(id, field)` for single-field indexed queries or `list(criteria)` for multi-field queries. Never use `pick()` in authorization checks.
123
+ - **`Resource.take()` is destructive** — it deletes the record it returns. For read-only identity lookups in gates or handlers use `load(where)`, which answers a multi-field query in one call (`load({ type, userId, credential })`), or `list(where, opts)` when you need every match. Never use `take()` in authorization checks.
124
124
  - **`IdentityLinkingService` is compatible with `AccountLinkingService`** from `@owlmeans/server-oidc-rp` but defined independently to avoid circular dependency. Both return `AuthPayload`.
125
125
  - First-login profiles are created with `ALL_SCOPES` and `AuthRole.User` — the gate layer is initially permissive per entity. Restrict scopes as needed for fine-grained authorization.
126
126
  - Account `credential` slug generation retries up to 5 times on duplicate key collision — this is the designed behavior for the Base58 slug space.
package/build/consts.d.ts CHANGED
@@ -2,10 +2,13 @@ export declare const AUTH_IDENTITY_ACCOUNT = "auth-identity:account";
2
2
  export declare const AUTH_IDENTITY_PROFILE = "auth-identity:profile";
3
3
  export declare const AUTH_IDENTITY_CREDENTIALS = "auth-identity:credentials";
4
4
  export declare const AUTH_IDENTITY_LINKING = "auth-identity:linking";
5
+ export declare const AUTH_IDENTITY_ORG_ENTITY = "auth-identity:org-entity";
5
6
  export declare const AUTH_IDENTITY_ACCOUNT_COLLECTION = "account";
6
7
  export declare const AUTH_IDENTITY_PROFILE_COLLECTION = "profile";
7
8
  export declare const AUTH_IDENTITY_CREDENTIALS_COLLECTION = "credentials";
9
+ export declare const AUTH_IDENTITY_ORG_ENTITY_COLLECTION = "org-entity";
8
10
  export declare const AUTH_IDENTITY_DB_ALIAS = "auth-identity";
11
+ export declare const MAX_ENTITY_SLUG_ATTEMPTS = 8;
9
12
  export declare const LOGIN_SERVICE_PREFIX = "service";
10
13
  export declare const EXTERNAL_KEY_DELIMITER = ":";
11
14
  //# sourceMappingURL=consts.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"consts.d.ts","sourceRoot":"","sources":["../src/consts.ts"],"names":[],"mappings":"AAKA,eAAO,MAAM,qBAAqB,0BAA0B,CAAA;AAC5D,eAAO,MAAM,qBAAqB,0BAA0B,CAAA;AAC5D,eAAO,MAAM,yBAAyB,8BAA8B,CAAA;AACpE,eAAO,MAAM,qBAAqB,0BAA0B,CAAA;AAM5D,eAAO,MAAM,gCAAgC,YAAY,CAAA;AACzD,eAAO,MAAM,gCAAgC,YAAY,CAAA;AACzD,eAAO,MAAM,oCAAoC,gBAAgB,CAAA;AAIjE,eAAO,MAAM,sBAAsB,kBAAkB,CAAA;AAGrD,eAAO,MAAM,oBAAoB,YAAY,CAAA;AAC7C,eAAO,MAAM,sBAAsB,MAAM,CAAA"}
1
+ {"version":3,"file":"consts.d.ts","sourceRoot":"","sources":["../src/consts.ts"],"names":[],"mappings":"AAKA,eAAO,MAAM,qBAAqB,0BAA0B,CAAA;AAC5D,eAAO,MAAM,qBAAqB,0BAA0B,CAAA;AAC5D,eAAO,MAAM,yBAAyB,8BAA8B,CAAA;AACpE,eAAO,MAAM,qBAAqB,0BAA0B,CAAA;AAC5D,eAAO,MAAM,wBAAwB,6BAA6B,CAAA;AAMlE,eAAO,MAAM,gCAAgC,YAAY,CAAA;AACzD,eAAO,MAAM,gCAAgC,YAAY,CAAA;AACzD,eAAO,MAAM,oCAAoC,gBAAgB,CAAA;AACjE,eAAO,MAAM,mCAAmC,eAAe,CAAA;AAI/D,eAAO,MAAM,sBAAsB,kBAAkB,CAAA;AAIrD,eAAO,MAAM,wBAAwB,IAAI,CAAA;AAGzC,eAAO,MAAM,oBAAoB,YAAY,CAAA;AAC7C,eAAO,MAAM,sBAAsB,MAAM,CAAA"}
package/build/consts.js CHANGED
@@ -6,6 +6,7 @@ export const AUTH_IDENTITY_ACCOUNT = 'auth-identity:account';
6
6
  export const AUTH_IDENTITY_PROFILE = 'auth-identity:profile';
7
7
  export const AUTH_IDENTITY_CREDENTIALS = 'auth-identity:credentials';
8
8
  export const AUTH_IDENTITY_LINKING = 'auth-identity:linking';
9
+ export const AUTH_IDENTITY_ORG_ENTITY = 'auth-identity:org-entity';
9
10
  // Colon-free Mongo collection base names. The final collection name is
10
11
  // `${resourcePrefix}${collection}` — e.g. with the project prefix 'viam-' this yields
11
12
  // 'viam-account'. Register the identity resources against a db config that carries the
@@ -13,9 +14,13 @@ export const AUTH_IDENTITY_LINKING = 'auth-identity:linking';
13
14
  export const AUTH_IDENTITY_ACCOUNT_COLLECTION = 'account';
14
15
  export const AUTH_IDENTITY_PROFILE_COLLECTION = 'profile';
15
16
  export const AUTH_IDENTITY_CREDENTIALS_COLLECTION = 'credentials';
17
+ export const AUTH_IDENTITY_ORG_ENTITY_COLLECTION = 'org-entity';
16
18
  // Suggested db-config alias for a dedicated config whose resourcePrefix scopes the
17
19
  // collection prefix to identity collections only (not the whole database).
18
20
  export const AUTH_IDENTITY_DB_ALIAS = 'auth-identity';
21
+ // How many slugs to try before giving up on minting a free one. Collisions are settled against a
22
+ // unique index, so this bounds a retry loop rather than a probability.
23
+ export const MAX_ENTITY_SLUG_ATTEMPTS = 8;
19
24
  // Helper prefixes for stable key derivation
20
25
  export const LOGIN_SERVICE_PREFIX = 'service';
21
26
  export const EXTERNAL_KEY_DELIMITER = ':';
@@ -1 +1 @@
1
- {"version":3,"file":"consts.js","sourceRoot":"","sources":["../src/consts.ts"],"names":[],"mappings":"AACA,wFAAwF;AACxF,sFAAsF;AACtF,sFAAsF;AACtF,uFAAuF;AACvF,MAAM,CAAC,MAAM,qBAAqB,GAAG,uBAAuB,CAAA;AAC5D,MAAM,CAAC,MAAM,qBAAqB,GAAG,uBAAuB,CAAA;AAC5D,MAAM,CAAC,MAAM,yBAAyB,GAAG,2BAA2B,CAAA;AACpE,MAAM,CAAC,MAAM,qBAAqB,GAAG,uBAAuB,CAAA;AAE5D,uEAAuE;AACvE,sFAAsF;AACtF,uFAAuF;AACvF,2FAA2F;AAC3F,MAAM,CAAC,MAAM,gCAAgC,GAAG,SAAS,CAAA;AACzD,MAAM,CAAC,MAAM,gCAAgC,GAAG,SAAS,CAAA;AACzD,MAAM,CAAC,MAAM,oCAAoC,GAAG,aAAa,CAAA;AAEjE,mFAAmF;AACnF,2EAA2E;AAC3E,MAAM,CAAC,MAAM,sBAAsB,GAAG,eAAe,CAAA;AAErD,4CAA4C;AAC5C,MAAM,CAAC,MAAM,oBAAoB,GAAG,SAAS,CAAA;AAC7C,MAAM,CAAC,MAAM,sBAAsB,GAAG,GAAG,CAAA"}
1
+ {"version":3,"file":"consts.js","sourceRoot":"","sources":["../src/consts.ts"],"names":[],"mappings":"AACA,wFAAwF;AACxF,sFAAsF;AACtF,sFAAsF;AACtF,uFAAuF;AACvF,MAAM,CAAC,MAAM,qBAAqB,GAAG,uBAAuB,CAAA;AAC5D,MAAM,CAAC,MAAM,qBAAqB,GAAG,uBAAuB,CAAA;AAC5D,MAAM,CAAC,MAAM,yBAAyB,GAAG,2BAA2B,CAAA;AACpE,MAAM,CAAC,MAAM,qBAAqB,GAAG,uBAAuB,CAAA;AAC5D,MAAM,CAAC,MAAM,wBAAwB,GAAG,0BAA0B,CAAA;AAElE,uEAAuE;AACvE,sFAAsF;AACtF,uFAAuF;AACvF,2FAA2F;AAC3F,MAAM,CAAC,MAAM,gCAAgC,GAAG,SAAS,CAAA;AACzD,MAAM,CAAC,MAAM,gCAAgC,GAAG,SAAS,CAAA;AACzD,MAAM,CAAC,MAAM,oCAAoC,GAAG,aAAa,CAAA;AACjE,MAAM,CAAC,MAAM,mCAAmC,GAAG,YAAY,CAAA;AAE/D,mFAAmF;AACnF,2EAA2E;AAC3E,MAAM,CAAC,MAAM,sBAAsB,GAAG,eAAe,CAAA;AAErD,iGAAiG;AACjG,uEAAuE;AACvE,MAAM,CAAC,MAAM,wBAAwB,GAAG,CAAC,CAAA;AAEzC,4CAA4C;AAC5C,MAAM,CAAC,MAAM,oBAAoB,GAAG,SAAS,CAAA;AAC7C,MAAM,CAAC,MAAM,sBAAsB,GAAG,GAAG,CAAA"}
@@ -1 +1 @@
1
- {"version":3,"file":"helper.d.ts","sourceRoot":"","sources":["../src/helper.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAA;AAIrD,eAAO,MAAM,2BAA2B,YAC7B,YAAY,CAAC,GAAG,CAAC,YAChB,MAAM,KACf,IAKF,CAAA"}
1
+ {"version":3,"file":"helper.d.ts","sourceRoot":"","sources":["../src/helper.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAA;AAKrD,eAAO,MAAM,2BAA2B,GACtC,SAAS,YAAY,CAAC,GAAG,CAAC,EAC1B,UAAU,MAAM,KACf,IAUF,CAAA"}
package/build/helper.js CHANGED
@@ -1,9 +1,15 @@
1
- import { makeIdentityAccountResource, makeIdentityProfileResource, makeIdentityCredentialsResource } from './resource.js';
1
+ import { makeIdentityAccountResource, makeIdentityProfileResource, makeIdentityCredentialsResource, makeOrgEntityResource } from './resource.js';
2
2
  import { makeIdentityLinkingService } from './service.js';
3
+ import { makeEntityResolverService } from './resolver.js';
3
4
  export const appendAuthIdentityResources = (context, dbAlias) => {
4
5
  context.registerResource(makeIdentityAccountResource(dbAlias));
5
6
  context.registerResource(makeIdentityProfileResource(dbAlias));
6
7
  context.registerResource(makeIdentityCredentialsResource(dbAlias));
8
+ context.registerResource(makeOrgEntityResource(dbAlias));
7
9
  context.registerService(makeIdentityLinkingService());
10
+ // Registering the resolver is what tells the server boundary that this deployment HAS
11
+ // organizations: without it `request.entity` stays undefined and every consumer falls back to
12
+ // treating the token's slug as the only entity value there is.
13
+ context.registerService(makeEntityResolverService());
8
14
  };
9
15
  //# sourceMappingURL=helper.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"helper.js","sourceRoot":"","sources":["../src/helper.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,2BAA2B,EAAE,2BAA2B,EAAE,+BAA+B,EAAE,MAAM,eAAe,CAAA;AACzH,OAAO,EAAE,0BAA0B,EAAE,MAAM,cAAc,CAAA;AAEzD,MAAM,CAAC,MAAM,2BAA2B,GAAG,CACzC,OAA0B,EAC1B,OAAgB,EACV,EAAE;IACR,OAAO,CAAC,gBAAgB,CAAC,2BAA2B,CAAC,OAAO,CAAC,CAAC,CAAA;IAC9D,OAAO,CAAC,gBAAgB,CAAC,2BAA2B,CAAC,OAAO,CAAC,CAAC,CAAA;IAC9D,OAAO,CAAC,gBAAgB,CAAC,+BAA+B,CAAC,OAAO,CAAC,CAAC,CAAA;IAClE,OAAO,CAAC,eAAe,CAAC,0BAA0B,EAAE,CAAC,CAAA;AACvD,CAAC,CAAA"}
1
+ {"version":3,"file":"helper.js","sourceRoot":"","sources":["../src/helper.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,2BAA2B,EAAE,2BAA2B,EAAE,+BAA+B,EAAE,qBAAqB,EAAE,MAAM,eAAe,CAAA;AAChJ,OAAO,EAAE,0BAA0B,EAAE,MAAM,cAAc,CAAA;AACzD,OAAO,EAAE,yBAAyB,EAAE,MAAM,eAAe,CAAA;AAEzD,MAAM,CAAC,MAAM,2BAA2B,GAAG,CACzC,OAA0B,EAC1B,OAAgB,EACV,EAAE;IACR,OAAO,CAAC,gBAAgB,CAAC,2BAA2B,CAAC,OAAO,CAAC,CAAC,CAAA;IAC9D,OAAO,CAAC,gBAAgB,CAAC,2BAA2B,CAAC,OAAO,CAAC,CAAC,CAAA;IAC9D,OAAO,CAAC,gBAAgB,CAAC,+BAA+B,CAAC,OAAO,CAAC,CAAC,CAAA;IAClE,OAAO,CAAC,gBAAgB,CAAC,qBAAqB,CAAC,OAAO,CAAC,CAAC,CAAA;IACxD,OAAO,CAAC,eAAe,CAAC,0BAA0B,EAAE,CAAC,CAAA;IACrD,sFAAsF;IACtF,8FAA8F;IAC9F,+DAA+D;IAC/D,OAAO,CAAC,eAAe,CAAC,yBAAyB,EAAE,CAAC,CAAA;AACtD,CAAC,CAAA"}
package/build/index.d.ts CHANGED
@@ -2,6 +2,7 @@ export * from './consts.js';
2
2
  export type * from './types.js';
3
3
  export * from './resource.js';
4
4
  export * from './service.js';
5
+ export * from './resolver.js';
5
6
  export * from './helper.js';
6
7
  export * from './iam.js';
7
8
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,aAAa,CAAA;AAC3B,mBAAmB,YAAY,CAAA;AAC/B,cAAc,eAAe,CAAA;AAC7B,cAAc,cAAc,CAAA;AAC5B,cAAc,aAAa,CAAA;AAC3B,cAAc,UAAU,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,aAAa,CAAA;AAC3B,mBAAmB,YAAY,CAAA;AAC/B,cAAc,eAAe,CAAA;AAC7B,cAAc,cAAc,CAAA;AAC5B,cAAc,eAAe,CAAA;AAC7B,cAAc,aAAa,CAAA;AAC3B,cAAc,UAAU,CAAA"}
package/build/index.js CHANGED
@@ -1,6 +1,7 @@
1
1
  export * from './consts.js';
2
2
  export * from './resource.js';
3
3
  export * from './service.js';
4
+ export * from './resolver.js';
4
5
  export * from './helper.js';
5
6
  export * from './iam.js';
6
7
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,aAAa,CAAA;AAE3B,cAAc,eAAe,CAAA;AAC7B,cAAc,cAAc,CAAA;AAC5B,cAAc,aAAa,CAAA;AAC3B,cAAc,UAAU,CAAA"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,aAAa,CAAA;AAE3B,cAAc,eAAe,CAAA;AAC7B,cAAc,cAAc,CAAA;AAC5B,cAAc,eAAe,CAAA;AAC7B,cAAc,aAAa,CAAA;AAC3B,cAAc,UAAU,CAAA"}
@@ -0,0 +1,3 @@
1
+ import type { EntityResolverService } from '@owlmeans/auth-common';
2
+ export declare const makeEntityResolverService: (alias?: string) => EntityResolverService;
3
+ //# sourceMappingURL=resolver.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resolver.d.ts","sourceRoot":"","sources":["../src/resolver.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,qBAAqB,EAAgB,MAAM,uBAAuB,CAAA;AA6BhF,eAAO,MAAM,yBAAyB,GACpC,QAAO,MAAwB,KAC9B,qBAkIF,CAAA"}
@@ -0,0 +1,133 @@
1
+ import { appendContextual } from '@owlmeans/context';
2
+ import { ENTITY_RESOLVER, ENTITY_SLUG_PATTERN } from '@owlmeans/auth-common';
3
+ import { generateWordSlug, nextSlugCandidate } from '@owlmeans/basic-ids';
4
+ import { AUTH_IDENTITY_ORG_ENTITY, MAX_ENTITY_SLUG_ATTEMPTS } from './consts.js';
5
+ /**
6
+ * How long a resolution is trusted without re-reading the registry.
7
+ *
8
+ * Every authenticated request resolves an entity, so an uncached resolver would put a database
9
+ * round-trip in front of the whole API. The cost of the cache is a rename taking up to this long
10
+ * to be seen by other replicas — which is survivable precisely because the old slug keeps
11
+ * resolving through `formerSlugs` rather than failing.
12
+ */
13
+ const CACHE_TTL = 30_000;
14
+ /** A stored record as the contract exposes it. Persisted records always carry an id. */
15
+ const toRef = (entity) => entity == null ? null : {
16
+ id: entity.id, slug: entity.slug, formerSlugs: entity.formerSlugs, iamKey: entity.iamKey,
17
+ };
18
+ export const makeEntityResolverService = (alias = ENTITY_RESOLVER) => {
19
+ // Keyed by every name a caller might arrive with — id, slug, former slug, iamKey — so a hit
20
+ // costs one lookup regardless of which one it was.
21
+ const cache = new Map();
22
+ const now = () => Date.now();
23
+ const remember = (entity, ...keys) => {
24
+ const at = now();
25
+ for (const key of keys) {
26
+ if (key !== '')
27
+ cache.set(key, { entity, at });
28
+ }
29
+ if (entity != null) {
30
+ cache.set(entity.id, { entity, at });
31
+ cache.set(entity.slug, { entity, at });
32
+ cache.set(entity.iamKey, { entity, at });
33
+ for (const former of entity.formerSlugs ?? [])
34
+ cache.set(former, { entity, at });
35
+ }
36
+ return entity;
37
+ };
38
+ const forget = (entity) => {
39
+ for (const key of [entity.id, entity.slug, entity.iamKey, ...(entity.formerSlugs ?? [])]) {
40
+ if (key != null)
41
+ cache.delete(key);
42
+ }
43
+ };
44
+ const resource = () => service.ctx.resource(AUTH_IDENTITY_ORG_ENTITY);
45
+ const load = async (value) => {
46
+ const cached = cache.get(value);
47
+ if (cached != null && now() - cached.at < CACHE_TTL) {
48
+ return cached.entity;
49
+ }
50
+ const res = resource();
51
+ // Id first: an id is authoritative, so a slug that happens to look like some other entity's
52
+ // id can never shadow it. A value that is not a record id simply misses. Then the current
53
+ // slug, then the names it has retired, then the frozen key external systems still quote.
54
+ const byId = await res.load(value);
55
+ if (byId != null)
56
+ return remember(byId, value);
57
+ const candidates = [
58
+ { slug: value },
59
+ { formerSlugs: { $contains: [value] } },
60
+ { iamKey: value },
61
+ ];
62
+ for (const where of candidates) {
63
+ const found = await res.load(where);
64
+ if (found != null)
65
+ return remember(found, value);
66
+ }
67
+ return remember(null, value);
68
+ };
69
+ const service = appendContextual(alias, {
70
+ resolve: async (value) => toRef(await load(value)),
71
+ byId: async (id) => toRef(remember(await resource().load(id), id)),
72
+ mintSlug: async () => {
73
+ const res = resource();
74
+ // A word slug is picked from 2^22 pairs, so a collision is rare — but "rare" is not "never"
75
+ // once an installation has many organizations, and the numeric suffix keeps the readable
76
+ // name rather than rerolling into an unrelated one.
77
+ for (let attempt = 1; attempt <= MAX_ENTITY_SLUG_ATTEMPTS; ++attempt) {
78
+ const base = generateWordSlug();
79
+ for (let suffix = 1; suffix <= 3; ++suffix) {
80
+ const candidate = nextSlugCandidate(base, suffix);
81
+ const taken = await res.load({
82
+ $or: [{ slug: candidate }, { formerSlugs: { $contains: [candidate] } }],
83
+ });
84
+ if (taken == null)
85
+ return candidate;
86
+ }
87
+ }
88
+ throw new SyntaxError('entity:slug-exhausted');
89
+ },
90
+ rename: async (id, slug) => {
91
+ if (!ENTITY_SLUG_PATTERN.test(slug)) {
92
+ throw new SyntaxError(`entity:slug-malformed:${slug}`);
93
+ }
94
+ const res = resource();
95
+ const entity = await res.get(id);
96
+ if (entity.slug === slug) {
97
+ return toRef(entity);
98
+ }
99
+ // A slug another entity has ever answered to cannot be taken: tokens and third-party records
100
+ // quoting it would start resolving to the wrong organization.
101
+ const taken = await res.load({
102
+ $or: [{ slug }, { formerSlugs: { $contains: [slug] } }],
103
+ });
104
+ if (taken != null && taken.id !== id) {
105
+ throw new SyntaxError(`entity:slug-taken:${slug}`);
106
+ }
107
+ forget(entity);
108
+ const formerSlugs = [...new Set([...(entity.formerSlugs ?? []), entity.slug])]
109
+ .filter(former => former !== slug);
110
+ const updated = await res.update({ ...entity, slug, formerSlugs, updatedAt: new Date() });
111
+ return toRef(remember(updated));
112
+ },
113
+ mintName: async (id, key, mint) => {
114
+ const res = resource();
115
+ const entity = await res.get(id);
116
+ const existing = entity.names?.[key];
117
+ // Read-before-mint is what freezes the name: the second caller gets the first caller's
118
+ // answer, so a rename between the two cannot produce two names for one live resource.
119
+ if (existing != null && existing !== '') {
120
+ return existing;
121
+ }
122
+ const minted = mint(toRef(entity));
123
+ forget(entity);
124
+ const updated = await res.update({
125
+ ...entity, names: { ...entity.names, [key]: minted }, updatedAt: new Date(),
126
+ });
127
+ remember(updated);
128
+ return updated.names?.[key] ?? minted;
129
+ },
130
+ });
131
+ return service;
132
+ };
133
+ //# sourceMappingURL=resolver.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resolver.js","sourceRoot":"","sources":["../src/resolver.ts"],"names":[],"mappings":"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"}
@@ -1,4 +1,14 @@
1
- import type { IdentityAccountResource, IdentityProfileResource, IdentityCredentialsResource } from './types.js';
1
+ import type { IdentityAccountResource, IdentityProfileResource, IdentityCredentialsResource, OrgEntityResource } from './types.js';
2
+ /**
3
+ * The organization-entity registry.
4
+ *
5
+ * Both unique indexes are load-bearing. `slug` is what a rename writes, so the index is the only
6
+ * thing standing between two organizations and the same public name; `iamKey` is quoted by systems
7
+ * that were told it once and never asked again, so a duplicate there would silently hand one
8
+ * organization another's realm. `formerSlugs` is indexed but NOT unique-scoped as a set — a retired
9
+ * slug must stay findable, and uniqueness against it is enforced when a rename is applied.
10
+ */
11
+ export declare const makeOrgEntityResource: (dbAlias?: string) => OrgEntityResource;
2
12
  export declare const makeIdentityAccountResource: (dbAlias?: string) => IdentityAccountResource;
3
13
  export declare const makeIdentityProfileResource: (dbAlias?: string) => IdentityProfileResource;
4
14
  export declare const makeIdentityCredentialsResource: (dbAlias?: string) => IdentityCredentialsResource;
@@ -1 +1 @@
1
- {"version":3,"file":"resource.d.ts","sourceRoot":"","sources":["../src/resource.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAmB,uBAAuB,EAAmB,uBAAuB,EAAuB,2BAA2B,EAAE,MAAM,YAAY,CAAA;AAMtK,eAAO,MAAM,2BAA2B,aAAc,MAAM,KAAG,uBAQ9D,CAAA;AAED,eAAO,MAAM,2BAA2B,aAAc,MAAM,KAAG,uBAe9D,CAAA;AAED,eAAO,MAAM,+BAA+B,aAAc,MAAM,KAAG,2BAOlE,CAAA"}
1
+ {"version":3,"file":"resource.d.ts","sourceRoot":"","sources":["../src/resource.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAmB,uBAAuB,EAAmB,uBAAuB,EAAuB,2BAA2B,EAAa,iBAAiB,EAAE,MAAM,YAAY,CAAA;AAOpM;;;;;;;;GAQG;AACH,eAAO,MAAM,qBAAqB,GAAI,UAAU,MAAM,KAAG,iBAQxD,CAAA;AAED,eAAO,MAAM,2BAA2B,GAAI,UAAU,MAAM,KAAG,uBAQ9D,CAAA;AAED,eAAO,MAAM,2BAA2B,GAAI,UAAU,MAAM,KAAG,uBAe9D,CAAA;AAED,eAAO,MAAM,+BAA+B,GAAI,UAAU,MAAM,KAAG,2BAOlE,CAAA"}
package/build/resource.js CHANGED
@@ -1,14 +1,30 @@
1
1
  import { makeMongoResource } from '@owlmeans/mongo-resource';
2
- import { AUTH_IDENTITY_ACCOUNT, AUTH_IDENTITY_PROFILE, AUTH_IDENTITY_CREDENTIALS, AUTH_IDENTITY_ACCOUNT_COLLECTION, AUTH_IDENTITY_PROFILE_COLLECTION, AUTH_IDENTITY_CREDENTIALS_COLLECTION } from './consts.js';
2
+ import { AUTH_IDENTITY_ACCOUNT, AUTH_IDENTITY_PROFILE, AUTH_IDENTITY_CREDENTIALS, AUTH_IDENTITY_ORG_ENTITY, AUTH_IDENTITY_ACCOUNT_COLLECTION, AUTH_IDENTITY_PROFILE_COLLECTION, AUTH_IDENTITY_CREDENTIALS_COLLECTION, AUTH_IDENTITY_ORG_ENTITY_COLLECTION } from './consts.js';
3
+ /**
4
+ * The organization-entity registry.
5
+ *
6
+ * Both unique indexes are load-bearing. `slug` is what a rename writes, so the index is the only
7
+ * thing standing between two organizations and the same public name; `iamKey` is quoted by systems
8
+ * that were told it once and never asked again, so a duplicate there would silently hand one
9
+ * organization another's realm. `formerSlugs` is indexed but NOT unique-scoped as a set — a retired
10
+ * slug must stay findable, and uniqueness against it is enforced when a rename is applied.
11
+ */
12
+ export const makeOrgEntityResource = (dbAlias) => {
13
+ const resource = makeMongoResource(AUTH_IDENTITY_ORG_ENTITY, dbAlias, undefined, AUTH_IDENTITY_ORG_ENTITY_COLLECTION);
14
+ resource.index('slug', { slug: 1 }, { unique: true });
15
+ resource.index('iamKey', { iamKey: 1 }, { unique: true });
16
+ resource.index('formerSlugs', { formerSlugs: 1 });
17
+ return resource;
18
+ };
3
19
  export const makeIdentityAccountResource = (dbAlias) => {
4
- const resource = makeMongoResource(AUTH_IDENTITY_ACCOUNT, dbAlias, undefined, undefined, AUTH_IDENTITY_ACCOUNT_COLLECTION);
20
+ const resource = makeMongoResource(AUTH_IDENTITY_ACCOUNT, dbAlias, undefined, AUTH_IDENTITY_ACCOUNT_COLLECTION);
5
21
  resource.index('credential', { credential: 1 }, { unique: true });
6
22
  resource.index('entityId', { entityId: 1 });
7
23
  resource.index('secret', { secret: 1 }, { unique: true, sparse: true });
8
24
  return resource;
9
25
  };
10
26
  export const makeIdentityProfileResource = (dbAlias) => {
11
- const resource = makeMongoResource(AUTH_IDENTITY_PROFILE, dbAlias, undefined, undefined, AUTH_IDENTITY_PROFILE_COLLECTION);
27
+ const resource = makeMongoResource(AUTH_IDENTITY_PROFILE, dbAlias, undefined, AUTH_IDENTITY_PROFILE_COLLECTION);
12
28
  /**
13
29
  * The account's mongo id — the ONLY ObjectId reference in the identity trio.
14
30
  * `profileId` is a composite key (`"{type}:{accountId}"`), `entityId` is the Base58
@@ -22,7 +38,7 @@ export const makeIdentityProfileResource = (dbAlias) => {
22
38
  return resource;
23
39
  };
24
40
  export const makeIdentityCredentialsResource = (dbAlias) => {
25
- const resource = makeMongoResource(AUTH_IDENTITY_CREDENTIALS, dbAlias, undefined, undefined, AUTH_IDENTITY_CREDENTIALS_COLLECTION);
41
+ const resource = makeMongoResource(AUTH_IDENTITY_CREDENTIALS, dbAlias, undefined, AUTH_IDENTITY_CREDENTIALS_COLLECTION);
26
42
  resource.index('provider', { type: 1, userId: 1, credential: 1 }, { unique: true });
27
43
  resource.index('profileId', { profileId: 1 });
28
44
  return resource;
@@ -1 +1 @@
1
- {"version":3,"file":"resource.js","sourceRoot":"","sources":["../src/resource.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,MAAM,0BAA0B,CAAA;AAE5D,OAAO,EACL,qBAAqB,EAAE,qBAAqB,EAAE,yBAAyB,EACvE,gCAAgC,EAAE,gCAAgC,EAAE,oCAAoC,EACzG,MAAM,aAAa,CAAA;AAEpB,MAAM,CAAC,MAAM,2BAA2B,GAAG,CAAC,OAAgB,EAA2B,EAAE;IACvF,MAAM,QAAQ,GAAG,iBAAiB,CAChC,qBAAqB,EAAE,OAAO,EAAE,SAAS,EAAE,SAAS,EAAE,gCAAgC,CACvF,CAAA;IACD,QAAQ,CAAC,KAAK,CAAC,YAAY,EAAE,EAAE,UAAU,EAAE,CAAC,EAAE,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,CAAA;IACjE,QAAQ,CAAC,KAAK,CAAC,UAAU,EAAE,EAAE,QAAQ,EAAE,CAAC,EAAE,CAAC,CAAA;IAC3C,QAAQ,CAAC,KAAK,CAAC,QAAQ,EAAE,EAAE,MAAM,EAAE,CAAC,EAAE,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,CAAA;IACvE,OAAO,QAAQ,CAAA;AACjB,CAAC,CAAA;AAED,MAAM,CAAC,MAAM,2BAA2B,GAAG,CAAC,OAAgB,EAA2B,EAAE;IACvF,MAAM,QAAQ,GAAG,iBAAiB,CAChC,qBAAqB,EAAE,OAAO,EAAE,SAAS,EAAE,SAAS,EAAE,gCAAgC,CACvF,CAAA;IACD;;;;OAIG;IACH,QAAQ,CAAC,SAAS,CAAC,QAAQ,EAAE,qBAAqB,CAAC,CAAA;IACnD,QAAQ,CAAC,KAAK,CAAC,QAAQ,EAAE,EAAE,MAAM,EAAE,CAAC,EAAE,CAAC,CAAA;IACvC,QAAQ,CAAC,KAAK,CAAC,UAAU,EAAE,EAAE,QAAQ,EAAE,CAAC,EAAE,CAAC,CAAA;IAC3C,QAAQ,CAAC,KAAK,CAAC,MAAM,EAAE,EAAE,IAAI,EAAE,CAAC,EAAE,QAAQ,EAAE,CAAC,EAAE,CAAC,CAAA;IAChD,QAAQ,CAAC,KAAK,CAAC,SAAS,EAAE,EAAE,SAAS,EAAE,CAAC,EAAE,QAAQ,EAAE,CAAC,EAAE,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,CAAA;IAC1E,OAAO,QAAQ,CAAA;AACjB,CAAC,CAAA;AAED,MAAM,CAAC,MAAM,+BAA+B,GAAG,CAAC,OAAgB,EAA+B,EAAE;IAC/F,MAAM,QAAQ,GAAG,iBAAiB,CAChC,yBAAyB,EAAE,OAAO,EAAE,SAAS,EAAE,SAAS,EAAE,oCAAoC,CAC/F,CAAA;IACD,QAAQ,CAAC,KAAK,CAAC,UAAU,EAAE,EAAE,IAAI,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,UAAU,EAAE,CAAC,EAAE,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,CAAA;IACnF,QAAQ,CAAC,KAAK,CAAC,WAAW,EAAE,EAAE,SAAS,EAAE,CAAC,EAAE,CAAC,CAAA;IAC7C,OAAO,QAAQ,CAAA;AACjB,CAAC,CAAA"}
1
+ {"version":3,"file":"resource.js","sourceRoot":"","sources":["../src/resource.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,MAAM,0BAA0B,CAAA;AAE5D,OAAO,EACL,qBAAqB,EAAE,qBAAqB,EAAE,yBAAyB,EAAE,wBAAwB,EACjG,gCAAgC,EAAE,gCAAgC,EAAE,oCAAoC,EACxG,mCAAmC,EACpC,MAAM,aAAa,CAAA;AAEpB;;;;;;;;GAQG;AACH,MAAM,CAAC,MAAM,qBAAqB,GAAG,CAAC,OAAgB,EAAqB,EAAE;IAC3E,MAAM,QAAQ,GAAG,iBAAiB,CAChC,wBAAwB,EAAE,OAAO,EAAE,SAAS,EAAE,mCAAmC,CAClF,CAAA;IACD,QAAQ,CAAC,KAAK,CAAC,MAAM,EAAE,EAAE,IAAI,EAAE,CAAC,EAAE,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,CAAA;IACrD,QAAQ,CAAC,KAAK,CAAC,QAAQ,EAAE,EAAE,MAAM,EAAE,CAAC,EAAE,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,CAAA;IACzD,QAAQ,CAAC,KAAK,CAAC,aAAa,EAAE,EAAE,WAAW,EAAE,CAAC,EAAE,CAAC,CAAA;IACjD,OAAO,QAAQ,CAAA;AACjB,CAAC,CAAA;AAED,MAAM,CAAC,MAAM,2BAA2B,GAAG,CAAC,OAAgB,EAA2B,EAAE;IACvF,MAAM,QAAQ,GAAG,iBAAiB,CAChC,qBAAqB,EAAE,OAAO,EAAE,SAAS,EAAE,gCAAgC,CAC5E,CAAA;IACD,QAAQ,CAAC,KAAK,CAAC,YAAY,EAAE,EAAE,UAAU,EAAE,CAAC,EAAE,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,CAAA;IACjE,QAAQ,CAAC,KAAK,CAAC,UAAU,EAAE,EAAE,QAAQ,EAAE,CAAC,EAAE,CAAC,CAAA;IAC3C,QAAQ,CAAC,KAAK,CAAC,QAAQ,EAAE,EAAE,MAAM,EAAE,CAAC,EAAE,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,CAAA;IACvE,OAAO,QAAQ,CAAA;AACjB,CAAC,CAAA;AAED,MAAM,CAAC,MAAM,2BAA2B,GAAG,CAAC,OAAgB,EAA2B,EAAE;IACvF,MAAM,QAAQ,GAAG,iBAAiB,CAChC,qBAAqB,EAAE,OAAO,EAAE,SAAS,EAAE,gCAAgC,CAC5E,CAAA;IACD;;;;OAIG;IACH,QAAQ,CAAC,SAAS,CAAC,QAAQ,EAAE,qBAAqB,CAAC,CAAA;IACnD,QAAQ,CAAC,KAAK,CAAC,QAAQ,EAAE,EAAE,MAAM,EAAE,CAAC,EAAE,CAAC,CAAA;IACvC,QAAQ,CAAC,KAAK,CAAC,UAAU,EAAE,EAAE,QAAQ,EAAE,CAAC,EAAE,CAAC,CAAA;IAC3C,QAAQ,CAAC,KAAK,CAAC,MAAM,EAAE,EAAE,IAAI,EAAE,CAAC,EAAE,QAAQ,EAAE,CAAC,EAAE,CAAC,CAAA;IAChD,QAAQ,CAAC,KAAK,CAAC,SAAS,EAAE,EAAE,SAAS,EAAE,CAAC,EAAE,QAAQ,EAAE,CAAC,EAAE,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,CAAA;IAC1E,OAAO,QAAQ,CAAA;AACjB,CAAC,CAAA;AAED,MAAM,CAAC,MAAM,+BAA+B,GAAG,CAAC,OAAgB,EAA+B,EAAE;IAC/F,MAAM,QAAQ,GAAG,iBAAiB,CAChC,yBAAyB,EAAE,OAAO,EAAE,SAAS,EAAE,oCAAoC,CACpF,CAAA;IACD,QAAQ,CAAC,KAAK,CAAC,UAAU,EAAE,EAAE,IAAI,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,UAAU,EAAE,CAAC,EAAE,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,CAAA;IACnF,QAAQ,CAAC,KAAK,CAAC,WAAW,EAAE,EAAE,SAAS,EAAE,CAAC,EAAE,CAAC,CAAA;IAC7C,OAAO,QAAQ,CAAA;AACjB,CAAC,CAAA"}
@@ -1 +1 @@
1
- {"version":3,"file":"service.d.ts","sourceRoot":"","sources":["../src/service.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAA8F,sBAAsB,EAAE,MAAM,YAAY,CAAA;AAepJ,eAAO,MAAM,0BAA0B,QAAO,sBA6K7C,CAAA"}
1
+ {"version":3,"file":"service.d.ts","sourceRoot":"","sources":["../src/service.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAA8F,sBAAsB,EAAE,MAAM,YAAY,CAAA;AAmBpJ,eAAO,MAAM,0BAA0B,QAAO,sBAqM7C,CAAA"}
package/build/service.js CHANGED
@@ -1,29 +1,41 @@
1
1
  import { appendContextual } from '@owlmeans/context';
2
2
  import { ALL_SCOPES, AuthRole } from '@owlmeans/auth';
3
3
  import { createIdOfLength, IdStyle } from '@owlmeans/basic-ids';
4
- import { AUTH_IDENTITY_ACCOUNT, AUTH_IDENTITY_PROFILE, AUTH_IDENTITY_CREDENTIALS, AUTH_IDENTITY_LINKING, LOGIN_SERVICE_PREFIX, EXTERNAL_KEY_DELIMITER } from './consts.js';
4
+ import { ENTITY_RESOLVER } from '@owlmeans/auth-common';
5
+ import { AUTH_IDENTITY_ACCOUNT, AUTH_IDENTITY_PROFILE, AUTH_IDENTITY_CREDENTIALS, AUTH_IDENTITY_LINKING, AUTH_IDENTITY_ORG_ENTITY, LOGIN_SERVICE_PREFIX, EXTERNAL_KEY_DELIMITER } from './consts.js';
5
6
  const MAX_SLUG_RETRIES = 5;
6
7
  // Stable external login key: "{type}:{service}:{providerSub}"
7
8
  const externalKey = (details) => [details.type, details.service, details.userId].join(EXTERNAL_KEY_DELIMITER);
8
9
  // Login-service credential stored on profile and credentials: "service:{type}:{service}"
9
10
  const loginService = (details) => [LOGIN_SERVICE_PREFIX, details.type, details.service].join(EXTERNAL_KEY_DELIMITER);
10
11
  export const makeIdentityLinkingService = () => {
12
+ /**
13
+ * The wire value for a stored entity id.
14
+ *
15
+ * Records key on the id; everything this service hands back is an auth payload, and payloads
16
+ * carry the slug. An id that no longer resolves yields undefined rather than leaking the raw
17
+ * id onto the wire, where a consumer would mistake it for a slug and compose names from it.
18
+ */
19
+ const slugOf = async (entityId) => {
20
+ if (entityId == null || entityId === '')
21
+ return undefined;
22
+ const ctx = service.ctx;
23
+ const entity = await ctx.service(ENTITY_RESOLVER).byId(entityId);
24
+ return entity?.slug;
25
+ };
11
26
  const service = appendContextual(AUTH_IDENTITY_LINKING, {
12
27
  getLinkedProfile: async (details) => {
13
28
  const ctx = service.ctx;
14
29
  const credsResource = ctx.resource(AUTH_IDENTITY_CREDENTIALS);
15
- const { items: creds } = await credsResource.list({
16
- criteria: {
17
- type: details.type,
18
- userId: externalKey(details),
19
- credential: loginService(details),
20
- }
30
+ const cred = await credsResource.load({
31
+ type: details.type,
32
+ userId: externalKey(details),
33
+ credential: loginService(details),
21
34
  });
22
- const cred = creds[0] ?? null;
23
35
  if (cred == null)
24
36
  return null;
25
37
  const profileResource = ctx.resource(AUTH_IDENTITY_PROFILE);
26
- const profile = await profileResource.load(cred.profileId, 'profileId');
38
+ const profile = await profileResource.load({ profileId: cred.profileId });
27
39
  if (profile == null)
28
40
  return null;
29
41
  return {
@@ -31,7 +43,7 @@ export const makeIdentityLinkingService = () => {
31
43
  role: profile.role,
32
44
  userId: profile.userId ?? profile.profileId,
33
45
  profileId: profile.profileId,
34
- entityId: profile.entityId,
46
+ entitySlug: await slugOf(profile.entityId),
35
47
  scopes: profile.scopes,
36
48
  };
37
49
  },
@@ -40,19 +52,31 @@ export const makeIdentityLinkingService = () => {
40
52
  const accountResource = ctx.resource(AUTH_IDENTITY_ACCOUNT);
41
53
  const profileResource = ctx.resource(AUTH_IDENTITY_PROFILE);
42
54
  const credsResource = ctx.resource(AUTH_IDENTITY_CREDENTIALS);
43
- // Generate a unique local entity slug stored as the account credential.
55
+ const entityResource = ctx.resource(AUTH_IDENTITY_ORG_ENTITY);
56
+ const resolver = ctx.service(ENTITY_RESOLVER);
57
+ // Every account gets its own organization. The entity record is created FIRST because it
58
+ // owns the two values that outlive everything else here: the id the account and its profiles
59
+ // are keyed by, and the frozen `iamKey` that realms and object names are minted from.
60
+ const entity = await entityResource.create({
61
+ slug: await resolver.mintSlug(),
62
+ formerSlugs: [],
63
+ // Frozen at birth and never recomputed. It cannot be the slug — the slug moves — and it
64
+ // cannot be the record id, which does not exist until this create returns.
65
+ iamKey: createIdOfLength(16, IdStyle.Base58),
66
+ names: {},
67
+ createdAt: new Date(),
68
+ });
69
+ const entityId = entity.id;
44
70
  let accountId = null;
45
- let entityId = null;
46
71
  for (let i = 0; i < MAX_SLUG_RETRIES; i++) {
47
72
  const credential = createIdOfLength(16, IdStyle.Base58);
48
73
  try {
49
74
  const account = await accountResource.create({
50
75
  credential,
51
76
  name: meta.username,
52
- entityId: credential,
77
+ entityId,
53
78
  });
54
79
  accountId = account.id;
55
- entityId = account.credential;
56
80
  break;
57
81
  }
58
82
  catch (err) {
@@ -61,7 +85,7 @@ export const makeIdentityLinkingService = () => {
61
85
  throw err;
62
86
  }
63
87
  }
64
- if (accountId == null || entityId == null) {
88
+ if (accountId == null) {
65
89
  throw new Error('Failed to generate unique account credential after retries');
66
90
  }
67
91
  const profileId = [details.type, accountId].join(EXTERNAL_KEY_DELIMITER);
@@ -80,7 +104,7 @@ export const makeIdentityLinkingService = () => {
80
104
  }
81
105
  catch (err) {
82
106
  if (err?.code === 11000 || err?.message?.includes('duplicate')) {
83
- profile = await profileResource.get(profileId, 'profileId');
107
+ profile = await profileResource.get({ profileId });
84
108
  }
85
109
  else {
86
110
  throw err;
@@ -98,7 +122,7 @@ export const makeIdentityLinkingService = () => {
98
122
  role: profile.role,
99
123
  userId: profile.userId ?? profile.profileId,
100
124
  profileId: profile.profileId,
101
- entityId: profile.entityId,
125
+ entitySlug: await slugOf(profile.entityId),
102
126
  scopes: profile.scopes,
103
127
  };
104
128
  },
@@ -123,17 +147,17 @@ export const makeIdentityLinkingService = () => {
123
147
  getOwnerProfiles: async (entityId) => {
124
148
  const ctx = service.ctx;
125
149
  const profileResource = ctx.resource(AUTH_IDENTITY_PROFILE);
126
- const { items: profiles } = await profileResource.list({ criteria: { entityId } });
127
- return profiles.map(p => ({
150
+ const { items: profiles } = await profileResource.list({ entityId });
151
+ return await Promise.all(profiles.map(async (p) => ({
128
152
  id: p.profileId,
129
153
  name: p.name,
130
154
  credential: p.credential,
131
- entityId: p.entityId,
155
+ entitySlug: await slugOf(p.entityId),
132
156
  scopes: p.scopes,
133
157
  groups: p.groups,
134
158
  permissions: p.permissions,
135
159
  attributes: p.attributes,
136
- }));
160
+ })));
137
161
  },
138
162
  getOwnerCredentials: async (userId, entityId, type) => {
139
163
  const ctx = service.ctx;
@@ -150,17 +174,16 @@ export const makeIdentityLinkingService = () => {
150
174
  };
151
175
  if (type != null)
152
176
  credsFilter.type = type;
153
- const { items: creds } = await credsResource.list(credsFilter);
154
- if (creds.length === 0)
177
+ const cred = await credsResource.load(credsFilter);
178
+ if (cred == null)
155
179
  return undefined;
156
- const cred = creds[0];
157
180
  const profile = profiles.find(p => p.profileId === cred.profileId) ?? profiles[0];
158
181
  return {
159
182
  type: cred.type,
160
183
  role: profile.role,
161
184
  userId: profile.userId ?? profile.profileId,
162
185
  profileId: profile.profileId,
163
- entityId: profile.entityId,
186
+ entitySlug: await slugOf(profile.entityId),
164
187
  scopes: profile.scopes,
165
188
  challenge: cred.challenge,
166
189
  credential: cred.credential,