@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 +5 -4
- package/agent-meta/manifest.json +2 -2
- package/agent-meta/skills/server-auth-identity/SKILL.md +7 -7
- package/build/consts.d.ts +3 -0
- package/build/consts.d.ts.map +1 -1
- package/build/consts.js +5 -0
- package/build/consts.js.map +1 -1
- package/build/helper.d.ts.map +1 -1
- package/build/helper.js +7 -1
- package/build/helper.js.map +1 -1
- package/build/index.d.ts +1 -0
- package/build/index.d.ts.map +1 -1
- package/build/index.js +1 -0
- package/build/index.js.map +1 -1
- package/build/resolver.d.ts +3 -0
- package/build/resolver.d.ts.map +1 -0
- package/build/resolver.js +133 -0
- package/build/resolver.js.map +1 -0
- package/build/resource.d.ts +11 -1
- package/build/resource.d.ts.map +1 -1
- package/build/resource.js +20 -4
- package/build/resource.js.map +1 -1
- package/build/service.d.ts.map +1 -1
- package/build/service.js +48 -25
- package/build/service.js.map +1 -1
- package/build/types.d.ts +30 -3
- package/build/types.d.ts.map +1 -1
- package/package.json +10 -9
- package/src/consts.ts +6 -0
- package/src/helper.ts +7 -1
- package/src/index.ts +1 -0
- package/src/resolver.ts +165 -0
- package/src/resource.ts +26 -6
- package/src/service.ts +57 -28
- package/src/types.ts +32 -3
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
|
|
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
|
|
46
|
+
const profile = await profiles.load({ profileId })
|
|
47
|
+
const scoped = await profiles.load({ entityId, profileId })
|
|
47
48
|
|
|
48
|
-
const { items } = await profiles.list({ entityId
|
|
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.
|
|
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
|
|
package/agent-meta/manifest.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 2,
|
|
3
3
|
"package": "@owlmeans/server-auth-identity",
|
|
4
|
-
"version": "0.1.18-rc.
|
|
5
|
-
"generatedAt": "2026-
|
|
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.
|
|
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
|
-
//
|
|
58
|
-
const profile = await profileRes.load(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
|
-
//
|
|
61
|
-
const { items } = await profileRes.list({ entityId,
|
|
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.
|
|
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
|
package/build/consts.d.ts.map
CHANGED
|
@@ -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;
|
|
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 = ':';
|
package/build/consts.js.map
CHANGED
|
@@ -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;
|
|
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"}
|
package/build/helper.d.ts.map
CHANGED
|
@@ -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;
|
|
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
|
package/build/helper.js.map
CHANGED
|
@@ -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;
|
|
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
package/build/index.d.ts.map
CHANGED
|
@@ -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
package/build/index.js.map
CHANGED
|
@@ -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 @@
|
|
|
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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"}
|
package/build/resource.d.ts
CHANGED
|
@@ -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;
|
package/build/resource.d.ts.map
CHANGED
|
@@ -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;
|
|
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,
|
|
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,
|
|
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,
|
|
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;
|
package/build/resource.js.map
CHANGED
|
@@ -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,
|
|
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"}
|
package/build/service.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"service.d.ts","sourceRoot":"","sources":["../src/service.ts"],"names":[],"mappings":"
|
|
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 {
|
|
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
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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({
|
|
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
|
-
|
|
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
|
|
154
|
-
if (
|
|
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
|
-
|
|
186
|
+
entitySlug: await slugOf(profile.entityId),
|
|
164
187
|
scopes: profile.scopes,
|
|
165
188
|
challenge: cred.challenge,
|
|
166
189
|
credential: cred.credential,
|