@owlmeans/server-auth-identity 0.1.18-rc.8 → 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.
- package/agent-meta/manifest.json +2 -2
- package/agent-meta/skills/server-auth-identity/SKILL.md +1 -1
- 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 +138 -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 +17 -1
- package/build/resource.js.map +1 -1
- package/build/service.d.ts.map +1 -1
- package/build/service.js +39 -12
- 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 +8 -7
- package/src/consts.ts +6 -0
- package/src/helper.ts +7 -1
- package/src/index.ts +1 -0
- package/src/resolver.ts +171 -0
- package/src/resource.ts +23 -3
- package/src/service.ts +43 -12
- package/src/types.ts +32 -3
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-08-
|
|
4
|
+
"version": "0.1.18-rc.8",
|
|
5
|
+
"generatedAt": "2026-08-27T16:32:18.461Z",
|
|
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.8"` in `dependencies`
|
|
12
12
|
|
|
13
13
|
## Key Exports
|
|
14
14
|
|
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,YAC7B,YAAY,CAAC,GAAG,CAAC,YAChB,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":"AAEA,OAAO,KAAK,EAAE,qBAAqB,EAAgB,MAAM,uBAAuB,CAAA;AAgChF,eAAO,MAAM,yBAAyB,WAC7B,MAAM,KACZ,qBAsIF,CAAA"}
|
|
@@ -0,0 +1,138 @@
|
|
|
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
|
+
/** What a stored record id looks like. Anything else can only be a slug or a frozen key. */
|
|
15
|
+
const RECORD_ID = /^[0-9a-f]{24}$/i;
|
|
16
|
+
/** A stored record as the contract exposes it. Persisted records always carry an id. */
|
|
17
|
+
const toRef = (entity) => entity == null ? null : {
|
|
18
|
+
id: entity.id, slug: entity.slug, formerSlugs: entity.formerSlugs, iamKey: entity.iamKey,
|
|
19
|
+
};
|
|
20
|
+
export const makeEntityResolverService = (alias = ENTITY_RESOLVER) => {
|
|
21
|
+
// Keyed by every name a caller might arrive with — id, slug, former slug, iamKey — so a hit
|
|
22
|
+
// costs one lookup regardless of which one it was.
|
|
23
|
+
const cache = new Map();
|
|
24
|
+
const now = () => Date.now();
|
|
25
|
+
const remember = (entity, ...keys) => {
|
|
26
|
+
const at = now();
|
|
27
|
+
for (const key of keys) {
|
|
28
|
+
if (key !== '')
|
|
29
|
+
cache.set(key, { entity, at });
|
|
30
|
+
}
|
|
31
|
+
if (entity != null) {
|
|
32
|
+
cache.set(entity.id, { entity, at });
|
|
33
|
+
cache.set(entity.slug, { entity, at });
|
|
34
|
+
cache.set(entity.iamKey, { entity, at });
|
|
35
|
+
for (const former of entity.formerSlugs ?? [])
|
|
36
|
+
cache.set(former, { entity, at });
|
|
37
|
+
}
|
|
38
|
+
return entity;
|
|
39
|
+
};
|
|
40
|
+
const forget = (entity) => {
|
|
41
|
+
for (const key of [entity.id, entity.slug, entity.iamKey, ...(entity.formerSlugs ?? [])]) {
|
|
42
|
+
if (key != null)
|
|
43
|
+
cache.delete(key);
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
const resource = () => service.ctx.resource(AUTH_IDENTITY_ORG_ENTITY);
|
|
47
|
+
const load = async (value) => {
|
|
48
|
+
const cached = cache.get(value);
|
|
49
|
+
if (cached != null && now() - cached.at < CACHE_TTL) {
|
|
50
|
+
return cached.entity;
|
|
51
|
+
}
|
|
52
|
+
const res = resource();
|
|
53
|
+
// Id first: an id is authoritative, so a slug that happens to look like some other entity's
|
|
54
|
+
// id can never shadow it. Then the current slug, then the names it has retired, then the
|
|
55
|
+
// frozen key external systems still quote.
|
|
56
|
+
//
|
|
57
|
+
// The shape test is not an optimisation. `load` reads its argument as a record id, and the
|
|
58
|
+
// store turns that into an ObjectId unconditionally — so handing it a slug throws a driver
|
|
59
|
+
// error out of what is meant to be a lookup that simply misses.
|
|
60
|
+
const byId = RECORD_ID.test(value) ? await res.load(value) : null;
|
|
61
|
+
if (byId != null)
|
|
62
|
+
return remember(byId, value);
|
|
63
|
+
for (const field of ['slug', 'formerSlugs', 'iamKey']) {
|
|
64
|
+
const { items } = await res.list({ criteria: { [field]: value }, pager: { size: 1 } });
|
|
65
|
+
if (items[0] != null)
|
|
66
|
+
return remember(items[0], value);
|
|
67
|
+
}
|
|
68
|
+
return remember(null, value);
|
|
69
|
+
};
|
|
70
|
+
const service = appendContextual(alias, {
|
|
71
|
+
resolve: async (value) => toRef(await load(value)),
|
|
72
|
+
byId: async (id) => {
|
|
73
|
+
const entity = RECORD_ID.test(id) ? await resource().load(id) : null;
|
|
74
|
+
return toRef(remember(entity, id));
|
|
75
|
+
},
|
|
76
|
+
mintSlug: async () => {
|
|
77
|
+
const res = resource();
|
|
78
|
+
// A word slug is picked from 2^22 pairs, so a collision is rare — but "rare" is not "never"
|
|
79
|
+
// once an installation has many organizations, and the numeric suffix keeps the readable
|
|
80
|
+
// name rather than rerolling into an unrelated one.
|
|
81
|
+
for (let attempt = 1; attempt <= MAX_ENTITY_SLUG_ATTEMPTS; ++attempt) {
|
|
82
|
+
const base = generateWordSlug();
|
|
83
|
+
for (let suffix = 1; suffix <= 3; ++suffix) {
|
|
84
|
+
const candidate = nextSlugCandidate(base, suffix);
|
|
85
|
+
const { items } = await res.list({
|
|
86
|
+
criteria: { $or: [{ slug: candidate }, { formerSlugs: candidate }] },
|
|
87
|
+
pager: { size: 1 },
|
|
88
|
+
});
|
|
89
|
+
if (items.length === 0)
|
|
90
|
+
return candidate;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
throw new SyntaxError('entity:slug-exhausted');
|
|
94
|
+
},
|
|
95
|
+
rename: async (id, slug) => {
|
|
96
|
+
if (!ENTITY_SLUG_PATTERN.test(slug)) {
|
|
97
|
+
throw new SyntaxError(`entity:slug-malformed:${slug}`);
|
|
98
|
+
}
|
|
99
|
+
const res = resource();
|
|
100
|
+
const entity = await res.get(id);
|
|
101
|
+
if (entity.slug === slug) {
|
|
102
|
+
return toRef(entity);
|
|
103
|
+
}
|
|
104
|
+
// A slug another entity has ever answered to cannot be taken: tokens and third-party records
|
|
105
|
+
// quoting it would start resolving to the wrong organization.
|
|
106
|
+
const { items: taken } = await res.list({
|
|
107
|
+
criteria: { $or: [{ slug }, { formerSlugs: slug }] }, pager: { size: 1 },
|
|
108
|
+
});
|
|
109
|
+
if (taken[0] != null && taken[0].id !== id) {
|
|
110
|
+
throw new SyntaxError(`entity:slug-taken:${slug}`);
|
|
111
|
+
}
|
|
112
|
+
forget(entity);
|
|
113
|
+
const formerSlugs = [...new Set([...(entity.formerSlugs ?? []), entity.slug])]
|
|
114
|
+
.filter(former => former !== slug);
|
|
115
|
+
const updated = await res.update({ ...entity, slug, formerSlugs, updatedAt: new Date() });
|
|
116
|
+
return toRef(remember(updated));
|
|
117
|
+
},
|
|
118
|
+
mintName: async (id, key, mint) => {
|
|
119
|
+
const res = resource();
|
|
120
|
+
const entity = await res.get(id);
|
|
121
|
+
const existing = entity.names?.[key];
|
|
122
|
+
// Read-before-mint is what freezes the name: the second caller gets the first caller's
|
|
123
|
+
// answer, so a rename between the two cannot produce two names for one live resource.
|
|
124
|
+
if (existing != null && existing !== '') {
|
|
125
|
+
return existing;
|
|
126
|
+
}
|
|
127
|
+
const minted = mint(toRef(entity));
|
|
128
|
+
forget(entity);
|
|
129
|
+
const updated = await res.update({
|
|
130
|
+
...entity, names: { ...entity.names, [key]: minted }, updatedAt: new Date(),
|
|
131
|
+
});
|
|
132
|
+
remember(updated);
|
|
133
|
+
return updated.names?.[key] ?? minted;
|
|
134
|
+
},
|
|
135
|
+
});
|
|
136
|
+
return service;
|
|
137
|
+
};
|
|
138
|
+
//# 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,aAAc,MAAM,KAAG,iBAQxD,CAAA;AAED,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"}
|
package/build/resource.js
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
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, 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
20
|
const resource = makeMongoResource(AUTH_IDENTITY_ACCOUNT, dbAlias, undefined, undefined, AUTH_IDENTITY_ACCOUNT_COLLECTION);
|
|
5
21
|
resource.index('credential', { credential: 1 }, { unique: true });
|
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":"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"}
|
package/build/service.d.ts.map
CHANGED
|
@@ -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;
|
|
1
|
+
{"version":3,"file":"service.d.ts","sourceRoot":"","sources":["../src/service.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAA8F,sBAAsB,EAAE,MAAM,YAAY,CAAA;AAkBpJ,eAAO,MAAM,0BAA0B,QAAO,sBAyM7C,CAAA"}
|
package/build/service.js
CHANGED
|
@@ -1,13 +1,28 @@
|
|
|
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;
|
|
@@ -31,7 +46,7 @@ export const makeIdentityLinkingService = () => {
|
|
|
31
46
|
role: profile.role,
|
|
32
47
|
userId: profile.userId ?? profile.profileId,
|
|
33
48
|
profileId: profile.profileId,
|
|
34
|
-
|
|
49
|
+
entitySlug: await slugOf(profile.entityId),
|
|
35
50
|
scopes: profile.scopes,
|
|
36
51
|
};
|
|
37
52
|
},
|
|
@@ -40,19 +55,31 @@ export const makeIdentityLinkingService = () => {
|
|
|
40
55
|
const accountResource = ctx.resource(AUTH_IDENTITY_ACCOUNT);
|
|
41
56
|
const profileResource = ctx.resource(AUTH_IDENTITY_PROFILE);
|
|
42
57
|
const credsResource = ctx.resource(AUTH_IDENTITY_CREDENTIALS);
|
|
43
|
-
|
|
58
|
+
const entityResource = ctx.resource(AUTH_IDENTITY_ORG_ENTITY);
|
|
59
|
+
const resolver = ctx.service(ENTITY_RESOLVER);
|
|
60
|
+
// Every account gets its own organization. The entity record is created FIRST because it
|
|
61
|
+
// owns the two values that outlive everything else here: the id the account and its profiles
|
|
62
|
+
// are keyed by, and the frozen `iamKey` that realms and object names are minted from.
|
|
63
|
+
const entity = await entityResource.create({
|
|
64
|
+
slug: await resolver.mintSlug(),
|
|
65
|
+
formerSlugs: [],
|
|
66
|
+
// Frozen at birth and never recomputed. It cannot be the slug — the slug moves — and it
|
|
67
|
+
// cannot be the record id, which does not exist until this create returns.
|
|
68
|
+
iamKey: createIdOfLength(16, IdStyle.Base58),
|
|
69
|
+
names: {},
|
|
70
|
+
createdAt: new Date(),
|
|
71
|
+
});
|
|
72
|
+
const entityId = entity.id;
|
|
44
73
|
let accountId = null;
|
|
45
|
-
let entityId = null;
|
|
46
74
|
for (let i = 0; i < MAX_SLUG_RETRIES; i++) {
|
|
47
75
|
const credential = createIdOfLength(16, IdStyle.Base58);
|
|
48
76
|
try {
|
|
49
77
|
const account = await accountResource.create({
|
|
50
78
|
credential,
|
|
51
79
|
name: meta.username,
|
|
52
|
-
entityId
|
|
80
|
+
entityId,
|
|
53
81
|
});
|
|
54
82
|
accountId = account.id;
|
|
55
|
-
entityId = account.credential;
|
|
56
83
|
break;
|
|
57
84
|
}
|
|
58
85
|
catch (err) {
|
|
@@ -61,7 +88,7 @@ export const makeIdentityLinkingService = () => {
|
|
|
61
88
|
throw err;
|
|
62
89
|
}
|
|
63
90
|
}
|
|
64
|
-
if (accountId == null
|
|
91
|
+
if (accountId == null) {
|
|
65
92
|
throw new Error('Failed to generate unique account credential after retries');
|
|
66
93
|
}
|
|
67
94
|
const profileId = [details.type, accountId].join(EXTERNAL_KEY_DELIMITER);
|
|
@@ -98,7 +125,7 @@ export const makeIdentityLinkingService = () => {
|
|
|
98
125
|
role: profile.role,
|
|
99
126
|
userId: profile.userId ?? profile.profileId,
|
|
100
127
|
profileId: profile.profileId,
|
|
101
|
-
|
|
128
|
+
entitySlug: await slugOf(profile.entityId),
|
|
102
129
|
scopes: profile.scopes,
|
|
103
130
|
};
|
|
104
131
|
},
|
|
@@ -124,16 +151,16 @@ export const makeIdentityLinkingService = () => {
|
|
|
124
151
|
const ctx = service.ctx;
|
|
125
152
|
const profileResource = ctx.resource(AUTH_IDENTITY_PROFILE);
|
|
126
153
|
const { items: profiles } = await profileResource.list({ criteria: { entityId } });
|
|
127
|
-
return profiles.map(p => ({
|
|
154
|
+
return await Promise.all(profiles.map(async (p) => ({
|
|
128
155
|
id: p.profileId,
|
|
129
156
|
name: p.name,
|
|
130
157
|
credential: p.credential,
|
|
131
|
-
|
|
158
|
+
entitySlug: await slugOf(p.entityId),
|
|
132
159
|
scopes: p.scopes,
|
|
133
160
|
groups: p.groups,
|
|
134
161
|
permissions: p.permissions,
|
|
135
162
|
attributes: p.attributes,
|
|
136
|
-
}));
|
|
163
|
+
})));
|
|
137
164
|
},
|
|
138
165
|
getOwnerCredentials: async (userId, entityId, type) => {
|
|
139
166
|
const ctx = service.ctx;
|
|
@@ -160,7 +187,7 @@ export const makeIdentityLinkingService = () => {
|
|
|
160
187
|
role: profile.role,
|
|
161
188
|
userId: profile.userId ?? profile.profileId,
|
|
162
189
|
profileId: profile.profileId,
|
|
163
|
-
|
|
190
|
+
entitySlug: await slugOf(profile.entityId),
|
|
164
191
|
scopes: profile.scopes,
|
|
165
192
|
challenge: cred.challenge,
|
|
166
193
|
credential: cred.credential,
|
package/build/service.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"service.js","sourceRoot":"","sources":["../src/service.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAA;AAGpD,OAAO,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAA;AAErD,OAAO,EAAE,gBAAgB,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAA;
|
|
1
|
+
{"version":3,"file":"service.js","sourceRoot":"","sources":["../src/service.ts"],"names":[],"mappings":"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"}
|
package/build/types.d.ts
CHANGED
|
@@ -6,22 +6,28 @@ import type { AuthCredentials, AuthPayload, AuthRole, Profile } from '@owlmeans/
|
|
|
6
6
|
import type { ProviderProfileDetails } from '@owlmeans/oidc';
|
|
7
7
|
/**
|
|
8
8
|
* Local account record — one per user.
|
|
9
|
-
*
|
|
9
|
+
*
|
|
10
|
+
* `id` is the Mongo resource id; `credential` is the account's stable unique key. `entityId`
|
|
11
|
+
* (inherited from Profile below) holds the organization entity's record id — never its slug.
|
|
10
12
|
*/
|
|
11
|
-
export interface IdentityAccount extends Profile, ResourceRecord {
|
|
13
|
+
export interface IdentityAccount extends Omit<Profile, 'entitySlug'>, ResourceRecord {
|
|
12
14
|
id: string;
|
|
13
15
|
credential: string;
|
|
16
|
+
/** Stable organization-entity id. Renaming the organization does not touch this. */
|
|
17
|
+
entityId?: string;
|
|
14
18
|
}
|
|
15
19
|
/**
|
|
16
20
|
* Local profile record — ties a user to an entity with a role.
|
|
17
21
|
* Mirrors IAMProfile from @owlmeans/auth backend.
|
|
18
22
|
*/
|
|
19
|
-
export interface IdentityProfile extends Profile, ResourceRecord {
|
|
23
|
+
export interface IdentityProfile extends Omit<Profile, 'entitySlug'>, ResourceRecord {
|
|
20
24
|
id: string;
|
|
21
25
|
profileId: string;
|
|
22
26
|
userId?: string;
|
|
23
27
|
role: AuthRole;
|
|
24
28
|
expiresAt?: Date;
|
|
29
|
+
/** Stable organization-entity id — the value every per-organization query filters on. */
|
|
30
|
+
entityId?: string;
|
|
25
31
|
}
|
|
26
32
|
/**
|
|
27
33
|
* Provider credentials record — one per provider link per profile.
|
|
@@ -32,6 +38,27 @@ export interface IdentityProfile extends Profile, ResourceRecord {
|
|
|
32
38
|
export interface IdentityCredentials extends AuthCredentials, ResourceRecord {
|
|
33
39
|
profileId: string;
|
|
34
40
|
}
|
|
41
|
+
/**
|
|
42
|
+
* An organization entity — the record every per-organization store keys on.
|
|
43
|
+
*
|
|
44
|
+
* Its `id` is the stable entity id. The slug is the value users, URLs and tokens see, and it may
|
|
45
|
+
* move; nothing here except `slug` itself changes when it does, which is the entire point of the
|
|
46
|
+
* record existing.
|
|
47
|
+
*/
|
|
48
|
+
export interface OrgEntity extends ResourceRecord {
|
|
49
|
+
id?: string;
|
|
50
|
+
slug: string;
|
|
51
|
+
formerSlugs?: string[];
|
|
52
|
+
iamKey: string;
|
|
53
|
+
/**
|
|
54
|
+
* Names this organization is known by in systems that cannot rename: a Kubernetes namespace, a
|
|
55
|
+
* storage prefix, an IAM realm. Minted once through `mintName` and read back forever after.
|
|
56
|
+
*/
|
|
57
|
+
names?: Record<string, string>;
|
|
58
|
+
createdAt: Date;
|
|
59
|
+
updatedAt?: Date;
|
|
60
|
+
}
|
|
61
|
+
export type OrgEntityResource = MongoResource<OrgEntity>;
|
|
35
62
|
export type IdentityAccountResource = MongoResource<IdentityAccount>;
|
|
36
63
|
export type IdentityProfileResource = MongoResource<IdentityProfile>;
|
|
37
64
|
export type IdentityCredentialsResource = MongoResource<IdentityCredentials>;
|
package/build/types.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAA;AACxD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,0BAA0B,CAAA;AAC7D,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAA;AAC3D,OAAO,KAAK,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,0BAA0B,CAAA;AAC3E,OAAO,KAAK,EAAE,eAAe,EAAE,WAAW,EAAE,QAAQ,EAAE,OAAO,EAAE,MAAM,gBAAgB,CAAA;AACrF,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,gBAAgB,CAAA;AAE5D
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@owlmeans/server-auth-identity",
|
|
3
|
-
"version": "0.1.18-rc.
|
|
3
|
+
"version": "0.1.18-rc.9",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"scripts": {
|
|
@@ -22,20 +22,21 @@
|
|
|
22
22
|
}
|
|
23
23
|
},
|
|
24
24
|
"dependencies": {
|
|
25
|
-
"@owlmeans/auth": "^0.1.18-rc.
|
|
26
|
-
"@owlmeans/
|
|
25
|
+
"@owlmeans/auth": "^0.1.18-rc.7",
|
|
26
|
+
"@owlmeans/auth-common": "^0.1.18-rc.9",
|
|
27
|
+
"@owlmeans/basic-ids": "^0.1.18-rc.7",
|
|
27
28
|
"@owlmeans/context": "^0.1.18-rc.6",
|
|
28
|
-
"@owlmeans/mongo-resource": "^0.1.18-rc.
|
|
29
|
-
"@owlmeans/oidc": "^0.1.18-rc.
|
|
29
|
+
"@owlmeans/mongo-resource": "^0.1.18-rc.8",
|
|
30
|
+
"@owlmeans/oidc": "^0.1.18-rc.9",
|
|
30
31
|
"@owlmeans/resource": "^0.1.18-rc.7",
|
|
31
|
-
"@owlmeans/server-context": "^0.1.18-rc.
|
|
32
|
+
"@owlmeans/server-context": "^0.1.18-rc.8"
|
|
32
33
|
},
|
|
33
34
|
"peerDependencies": {
|
|
34
35
|
"mongodb": "*"
|
|
35
36
|
},
|
|
36
37
|
"devDependencies": {
|
|
37
38
|
"@owlmeans/dep-config": "workspace:*",
|
|
38
|
-
"@types/bun": "^1.
|
|
39
|
+
"@types/bun": "^1.4.0",
|
|
39
40
|
"nodemon": "^3.1.14",
|
|
40
41
|
"typescript": "^7.0.2"
|
|
41
42
|
},
|
package/src/consts.ts
CHANGED
|
@@ -7,6 +7,7 @@ export const AUTH_IDENTITY_ACCOUNT = 'auth-identity:account'
|
|
|
7
7
|
export const AUTH_IDENTITY_PROFILE = 'auth-identity:profile'
|
|
8
8
|
export const AUTH_IDENTITY_CREDENTIALS = 'auth-identity:credentials'
|
|
9
9
|
export const AUTH_IDENTITY_LINKING = 'auth-identity:linking'
|
|
10
|
+
export const AUTH_IDENTITY_ORG_ENTITY = 'auth-identity:org-entity'
|
|
10
11
|
|
|
11
12
|
// Colon-free Mongo collection base names. The final collection name is
|
|
12
13
|
// `${resourcePrefix}${collection}` — e.g. with the project prefix 'viam-' this yields
|
|
@@ -15,11 +16,16 @@ export const AUTH_IDENTITY_LINKING = 'auth-identity:linking'
|
|
|
15
16
|
export const AUTH_IDENTITY_ACCOUNT_COLLECTION = 'account'
|
|
16
17
|
export const AUTH_IDENTITY_PROFILE_COLLECTION = 'profile'
|
|
17
18
|
export const AUTH_IDENTITY_CREDENTIALS_COLLECTION = 'credentials'
|
|
19
|
+
export const AUTH_IDENTITY_ORG_ENTITY_COLLECTION = 'org-entity'
|
|
18
20
|
|
|
19
21
|
// Suggested db-config alias for a dedicated config whose resourcePrefix scopes the
|
|
20
22
|
// collection prefix to identity collections only (not the whole database).
|
|
21
23
|
export const AUTH_IDENTITY_DB_ALIAS = 'auth-identity'
|
|
22
24
|
|
|
25
|
+
// How many slugs to try before giving up on minting a free one. Collisions are settled against a
|
|
26
|
+
// unique index, so this bounds a retry loop rather than a probability.
|
|
27
|
+
export const MAX_ENTITY_SLUG_ATTEMPTS = 8
|
|
28
|
+
|
|
23
29
|
// Helper prefixes for stable key derivation
|
|
24
30
|
export const LOGIN_SERVICE_PREFIX = 'service'
|
|
25
31
|
export const EXTERNAL_KEY_DELIMITER = ':'
|
package/src/helper.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { BasicContext } from '@owlmeans/context'
|
|
2
|
-
import { makeIdentityAccountResource, makeIdentityProfileResource, makeIdentityCredentialsResource } from './resource.js'
|
|
2
|
+
import { makeIdentityAccountResource, makeIdentityProfileResource, makeIdentityCredentialsResource, makeOrgEntityResource } from './resource.js'
|
|
3
3
|
import { makeIdentityLinkingService } from './service.js'
|
|
4
|
+
import { makeEntityResolverService } from './resolver.js'
|
|
4
5
|
|
|
5
6
|
export const appendAuthIdentityResources = (
|
|
6
7
|
context: BasicContext<any>,
|
|
@@ -9,5 +10,10 @@ export const appendAuthIdentityResources = (
|
|
|
9
10
|
context.registerResource(makeIdentityAccountResource(dbAlias))
|
|
10
11
|
context.registerResource(makeIdentityProfileResource(dbAlias))
|
|
11
12
|
context.registerResource(makeIdentityCredentialsResource(dbAlias))
|
|
13
|
+
context.registerResource(makeOrgEntityResource(dbAlias))
|
|
12
14
|
context.registerService(makeIdentityLinkingService())
|
|
15
|
+
// Registering the resolver is what tells the server boundary that this deployment HAS
|
|
16
|
+
// organizations: without it `request.entity` stays undefined and every consumer falls back to
|
|
17
|
+
// treating the token's slug as the only entity value there is.
|
|
18
|
+
context.registerService(makeEntityResolverService())
|
|
13
19
|
}
|
package/src/index.ts
CHANGED
package/src/resolver.ts
ADDED
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import { appendContextual } from '@owlmeans/context'
|
|
2
|
+
import type { ServerConfig, ServerContext } from '@owlmeans/server-context'
|
|
3
|
+
import type { EntityResolverService, OrgEntityRef } from '@owlmeans/auth-common'
|
|
4
|
+
import { ENTITY_RESOLVER, ENTITY_SLUG_PATTERN } from '@owlmeans/auth-common'
|
|
5
|
+
import { generateWordSlug, nextSlugCandidate } from '@owlmeans/basic-ids'
|
|
6
|
+
import type { OrgEntity, OrgEntityResource } from './types.js'
|
|
7
|
+
import { AUTH_IDENTITY_ORG_ENTITY, MAX_ENTITY_SLUG_ATTEMPTS } from './consts.js'
|
|
8
|
+
|
|
9
|
+
type Context = ServerContext<ServerConfig>
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* How long a resolution is trusted without re-reading the registry.
|
|
13
|
+
*
|
|
14
|
+
* Every authenticated request resolves an entity, so an uncached resolver would put a database
|
|
15
|
+
* round-trip in front of the whole API. The cost of the cache is a rename taking up to this long
|
|
16
|
+
* to be seen by other replicas — which is survivable precisely because the old slug keeps
|
|
17
|
+
* resolving through `formerSlugs` rather than failing.
|
|
18
|
+
*/
|
|
19
|
+
const CACHE_TTL = 30_000
|
|
20
|
+
|
|
21
|
+
/** What a stored record id looks like. Anything else can only be a slug or a frozen key. */
|
|
22
|
+
const RECORD_ID = /^[0-9a-f]{24}$/i
|
|
23
|
+
|
|
24
|
+
/** A stored record as the contract exposes it. Persisted records always carry an id. */
|
|
25
|
+
const toRef = (entity: OrgEntity | null): OrgEntityRef | null =>
|
|
26
|
+
entity == null ? null : {
|
|
27
|
+
id: entity.id!, slug: entity.slug, formerSlugs: entity.formerSlugs, iamKey: entity.iamKey,
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
interface CacheEntry {
|
|
31
|
+
entity: OrgEntity | null
|
|
32
|
+
at: number
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export const makeEntityResolverService = (
|
|
36
|
+
alias: string = ENTITY_RESOLVER
|
|
37
|
+
): EntityResolverService => {
|
|
38
|
+
// Keyed by every name a caller might arrive with — id, slug, former slug, iamKey — so a hit
|
|
39
|
+
// costs one lookup regardless of which one it was.
|
|
40
|
+
const cache = new Map<string, CacheEntry>()
|
|
41
|
+
const now = () => Date.now()
|
|
42
|
+
|
|
43
|
+
const remember = (entity: OrgEntity | null, ...keys: string[]): OrgEntity | null => {
|
|
44
|
+
const at = now()
|
|
45
|
+
for (const key of keys) {
|
|
46
|
+
if (key !== '') cache.set(key, { entity, at })
|
|
47
|
+
}
|
|
48
|
+
if (entity != null) {
|
|
49
|
+
cache.set(entity.id!, { entity, at })
|
|
50
|
+
cache.set(entity.slug, { entity, at })
|
|
51
|
+
cache.set(entity.iamKey, { entity, at })
|
|
52
|
+
for (const former of entity.formerSlugs ?? []) cache.set(former, { entity, at })
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return entity
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const forget = (entity: OrgEntity) => {
|
|
59
|
+
for (const key of [entity.id, entity.slug, entity.iamKey, ...(entity.formerSlugs ?? [])]) {
|
|
60
|
+
if (key != null) cache.delete(key)
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const resource = (): OrgEntityResource =>
|
|
65
|
+
(service.ctx as Context).resource<OrgEntityResource>(AUTH_IDENTITY_ORG_ENTITY)
|
|
66
|
+
|
|
67
|
+
const load = async (value: string): Promise<OrgEntity | null> => {
|
|
68
|
+
const cached = cache.get(value)
|
|
69
|
+
if (cached != null && now() - cached.at < CACHE_TTL) {
|
|
70
|
+
return cached.entity
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const res = resource()
|
|
74
|
+
// Id first: an id is authoritative, so a slug that happens to look like some other entity's
|
|
75
|
+
// id can never shadow it. Then the current slug, then the names it has retired, then the
|
|
76
|
+
// frozen key external systems still quote.
|
|
77
|
+
//
|
|
78
|
+
// The shape test is not an optimisation. `load` reads its argument as a record id, and the
|
|
79
|
+
// store turns that into an ObjectId unconditionally — so handing it a slug throws a driver
|
|
80
|
+
// error out of what is meant to be a lookup that simply misses.
|
|
81
|
+
const byId = RECORD_ID.test(value) ? await res.load(value) : null
|
|
82
|
+
if (byId != null) return remember(byId, value)
|
|
83
|
+
|
|
84
|
+
for (const field of ['slug', 'formerSlugs', 'iamKey'] as const) {
|
|
85
|
+
const { items } = await res.list({ criteria: { [field]: value } as never, pager: { size: 1 } })
|
|
86
|
+
if (items[0] != null) return remember(items[0], value)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return remember(null, value)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const service: EntityResolverService = appendContextual<EntityResolverService>(alias, {
|
|
93
|
+
resolve: async value => toRef(await load(value)),
|
|
94
|
+
|
|
95
|
+
byId: async id => {
|
|
96
|
+
const entity = RECORD_ID.test(id) ? await resource().load(id) : null
|
|
97
|
+
|
|
98
|
+
return toRef(remember(entity, id))
|
|
99
|
+
},
|
|
100
|
+
|
|
101
|
+
mintSlug: async () => {
|
|
102
|
+
const res = resource()
|
|
103
|
+
// A word slug is picked from 2^22 pairs, so a collision is rare — but "rare" is not "never"
|
|
104
|
+
// once an installation has many organizations, and the numeric suffix keeps the readable
|
|
105
|
+
// name rather than rerolling into an unrelated one.
|
|
106
|
+
for (let attempt = 1; attempt <= MAX_ENTITY_SLUG_ATTEMPTS; ++attempt) {
|
|
107
|
+
const base = generateWordSlug()
|
|
108
|
+
for (let suffix = 1; suffix <= 3; ++suffix) {
|
|
109
|
+
const candidate = nextSlugCandidate(base, suffix)
|
|
110
|
+
const { items } = await res.list({
|
|
111
|
+
criteria: { $or: [{ slug: candidate }, { formerSlugs: candidate }] } as never,
|
|
112
|
+
pager: { size: 1 },
|
|
113
|
+
})
|
|
114
|
+
if (items.length === 0) return candidate
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
throw new SyntaxError('entity:slug-exhausted')
|
|
119
|
+
},
|
|
120
|
+
|
|
121
|
+
rename: async (id, slug) => {
|
|
122
|
+
if (!ENTITY_SLUG_PATTERN.test(slug)) {
|
|
123
|
+
throw new SyntaxError(`entity:slug-malformed:${slug}`)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const res = resource()
|
|
127
|
+
const entity = await res.get(id)
|
|
128
|
+
if (entity.slug === slug) {
|
|
129
|
+
return toRef(entity)!
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// A slug another entity has ever answered to cannot be taken: tokens and third-party records
|
|
133
|
+
// quoting it would start resolving to the wrong organization.
|
|
134
|
+
const { items: taken } = await res.list({
|
|
135
|
+
criteria: { $or: [{ slug }, { formerSlugs: slug }] } as never, pager: { size: 1 },
|
|
136
|
+
})
|
|
137
|
+
if (taken[0] != null && taken[0].id !== id) {
|
|
138
|
+
throw new SyntaxError(`entity:slug-taken:${slug}`)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
forget(entity)
|
|
142
|
+
const formerSlugs = [...new Set([...(entity.formerSlugs ?? []), entity.slug])]
|
|
143
|
+
.filter(former => former !== slug)
|
|
144
|
+
const updated = await res.update({ ...entity, slug, formerSlugs, updatedAt: new Date() })
|
|
145
|
+
|
|
146
|
+
return toRef(remember(updated))!
|
|
147
|
+
},
|
|
148
|
+
|
|
149
|
+
mintName: async (id, key, mint) => {
|
|
150
|
+
const res = resource()
|
|
151
|
+
const entity = await res.get(id)
|
|
152
|
+
const existing = entity.names?.[key]
|
|
153
|
+
// Read-before-mint is what freezes the name: the second caller gets the first caller's
|
|
154
|
+
// answer, so a rename between the two cannot produce two names for one live resource.
|
|
155
|
+
if (existing != null && existing !== '') {
|
|
156
|
+
return existing
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const minted = mint(toRef(entity)!)
|
|
160
|
+
forget(entity)
|
|
161
|
+
const updated = await res.update({
|
|
162
|
+
...entity, names: { ...entity.names, [key]: minted }, updatedAt: new Date(),
|
|
163
|
+
})
|
|
164
|
+
remember(updated)
|
|
165
|
+
|
|
166
|
+
return updated.names?.[key] ?? minted
|
|
167
|
+
},
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
return service
|
|
171
|
+
}
|
package/src/resource.ts
CHANGED
|
@@ -1,10 +1,30 @@
|
|
|
1
1
|
import { makeMongoResource } from '@owlmeans/mongo-resource'
|
|
2
|
-
import type { IdentityAccount, IdentityAccountResource, IdentityProfile, IdentityProfileResource, IdentityCredentials, IdentityCredentialsResource } from './types.js'
|
|
2
|
+
import type { IdentityAccount, IdentityAccountResource, IdentityProfile, IdentityProfileResource, IdentityCredentials, IdentityCredentialsResource, OrgEntity, OrgEntityResource } from './types.js'
|
|
3
3
|
import {
|
|
4
|
-
AUTH_IDENTITY_ACCOUNT, AUTH_IDENTITY_PROFILE, AUTH_IDENTITY_CREDENTIALS,
|
|
5
|
-
AUTH_IDENTITY_ACCOUNT_COLLECTION, AUTH_IDENTITY_PROFILE_COLLECTION, AUTH_IDENTITY_CREDENTIALS_COLLECTION
|
|
4
|
+
AUTH_IDENTITY_ACCOUNT, AUTH_IDENTITY_PROFILE, AUTH_IDENTITY_CREDENTIALS, AUTH_IDENTITY_ORG_ENTITY,
|
|
5
|
+
AUTH_IDENTITY_ACCOUNT_COLLECTION, AUTH_IDENTITY_PROFILE_COLLECTION, AUTH_IDENTITY_CREDENTIALS_COLLECTION,
|
|
6
|
+
AUTH_IDENTITY_ORG_ENTITY_COLLECTION
|
|
6
7
|
} from './consts.js'
|
|
7
8
|
|
|
9
|
+
/**
|
|
10
|
+
* The organization-entity registry.
|
|
11
|
+
*
|
|
12
|
+
* Both unique indexes are load-bearing. `slug` is what a rename writes, so the index is the only
|
|
13
|
+
* thing standing between two organizations and the same public name; `iamKey` is quoted by systems
|
|
14
|
+
* that were told it once and never asked again, so a duplicate there would silently hand one
|
|
15
|
+
* organization another's realm. `formerSlugs` is indexed but NOT unique-scoped as a set — a retired
|
|
16
|
+
* slug must stay findable, and uniqueness against it is enforced when a rename is applied.
|
|
17
|
+
*/
|
|
18
|
+
export const makeOrgEntityResource = (dbAlias?: string): OrgEntityResource => {
|
|
19
|
+
const resource = makeMongoResource<OrgEntity, OrgEntityResource>(
|
|
20
|
+
AUTH_IDENTITY_ORG_ENTITY, dbAlias, undefined, undefined, AUTH_IDENTITY_ORG_ENTITY_COLLECTION
|
|
21
|
+
)
|
|
22
|
+
resource.index('slug', { slug: 1 }, { unique: true })
|
|
23
|
+
resource.index('iamKey', { iamKey: 1 }, { unique: true })
|
|
24
|
+
resource.index('formerSlugs', { formerSlugs: 1 })
|
|
25
|
+
return resource
|
|
26
|
+
}
|
|
27
|
+
|
|
8
28
|
export const makeIdentityAccountResource = (dbAlias?: string): IdentityAccountResource => {
|
|
9
29
|
const resource = makeMongoResource<IdentityAccount, IdentityAccountResource>(
|
|
10
30
|
AUTH_IDENTITY_ACCOUNT, dbAlias, undefined, undefined, AUTH_IDENTITY_ACCOUNT_COLLECTION
|
package/src/service.ts
CHANGED
|
@@ -5,7 +5,10 @@ import { ALL_SCOPES, AuthRole } from '@owlmeans/auth'
|
|
|
5
5
|
import type { ProviderProfileDetails } from '@owlmeans/oidc'
|
|
6
6
|
import { createIdOfLength, IdStyle } from '@owlmeans/basic-ids'
|
|
7
7
|
import type { AccountMeta, IdentityAccountResource, IdentityProfileResource, IdentityCredentialsResource, IdentityLinkingService } from './types.js'
|
|
8
|
-
import {
|
|
8
|
+
import type { OrgEntity, OrgEntityResource } from './types.js'
|
|
9
|
+
import type { EntityResolverService } from '@owlmeans/auth-common'
|
|
10
|
+
import { ENTITY_RESOLVER } from '@owlmeans/auth-common'
|
|
11
|
+
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'
|
|
9
12
|
|
|
10
13
|
type Context = ServerContext<ServerConfig>
|
|
11
14
|
|
|
@@ -20,6 +23,21 @@ const loginService = (details: ProviderProfileDetails): string =>
|
|
|
20
23
|
[LOGIN_SERVICE_PREFIX, details.type, details.service].join(EXTERNAL_KEY_DELIMITER)
|
|
21
24
|
|
|
22
25
|
export const makeIdentityLinkingService = (): IdentityLinkingService => {
|
|
26
|
+
/**
|
|
27
|
+
* The wire value for a stored entity id.
|
|
28
|
+
*
|
|
29
|
+
* Records key on the id; everything this service hands back is an auth payload, and payloads
|
|
30
|
+
* carry the slug. An id that no longer resolves yields undefined rather than leaking the raw
|
|
31
|
+
* id onto the wire, where a consumer would mistake it for a slug and compose names from it.
|
|
32
|
+
*/
|
|
33
|
+
const slugOf = async (entityId?: string): Promise<string | undefined> => {
|
|
34
|
+
if (entityId == null || entityId === '') return undefined
|
|
35
|
+
const ctx = service.ctx as Context
|
|
36
|
+
const entity = await ctx.service<EntityResolverService>(ENTITY_RESOLVER).byId(entityId)
|
|
37
|
+
|
|
38
|
+
return entity?.slug
|
|
39
|
+
}
|
|
40
|
+
|
|
23
41
|
const service: IdentityLinkingService = appendContextual<IdentityLinkingService>(AUTH_IDENTITY_LINKING, {
|
|
24
42
|
getLinkedProfile: async (details: ProviderProfileDetails): Promise<AuthPayload | null> => {
|
|
25
43
|
const ctx = service.ctx as Context
|
|
@@ -44,7 +62,7 @@ export const makeIdentityLinkingService = (): IdentityLinkingService => {
|
|
|
44
62
|
role: profile.role,
|
|
45
63
|
userId: profile.userId ?? profile.profileId,
|
|
46
64
|
profileId: profile.profileId,
|
|
47
|
-
|
|
65
|
+
entitySlug: await slugOf(profile.entityId),
|
|
48
66
|
scopes: profile.scopes,
|
|
49
67
|
}
|
|
50
68
|
},
|
|
@@ -54,20 +72,33 @@ export const makeIdentityLinkingService = (): IdentityLinkingService => {
|
|
|
54
72
|
const accountResource = ctx.resource<IdentityAccountResource>(AUTH_IDENTITY_ACCOUNT)
|
|
55
73
|
const profileResource = ctx.resource<IdentityProfileResource>(AUTH_IDENTITY_PROFILE)
|
|
56
74
|
const credsResource = ctx.resource<IdentityCredentialsResource>(AUTH_IDENTITY_CREDENTIALS)
|
|
75
|
+
const entityResource = ctx.resource<OrgEntityResource>(AUTH_IDENTITY_ORG_ENTITY)
|
|
76
|
+
const resolver = ctx.service<EntityResolverService>(ENTITY_RESOLVER)
|
|
77
|
+
|
|
78
|
+
// Every account gets its own organization. The entity record is created FIRST because it
|
|
79
|
+
// owns the two values that outlive everything else here: the id the account and its profiles
|
|
80
|
+
// are keyed by, and the frozen `iamKey` that realms and object names are minted from.
|
|
81
|
+
const entity: OrgEntity = await entityResource.create({
|
|
82
|
+
slug: await resolver.mintSlug(),
|
|
83
|
+
formerSlugs: [],
|
|
84
|
+
// Frozen at birth and never recomputed. It cannot be the slug — the slug moves — and it
|
|
85
|
+
// cannot be the record id, which does not exist until this create returns.
|
|
86
|
+
iamKey: createIdOfLength(16, IdStyle.Base58),
|
|
87
|
+
names: {},
|
|
88
|
+
createdAt: new Date(),
|
|
89
|
+
})
|
|
90
|
+
const entityId = entity.id!
|
|
57
91
|
|
|
58
|
-
// Generate a unique local entity slug stored as the account credential.
|
|
59
92
|
let accountId: string | null = null
|
|
60
|
-
let entityId: string | null = null
|
|
61
93
|
for (let i = 0; i < MAX_SLUG_RETRIES; i++) {
|
|
62
94
|
const credential = createIdOfLength(16, IdStyle.Base58)
|
|
63
95
|
try {
|
|
64
96
|
const account = await accountResource.create({
|
|
65
97
|
credential,
|
|
66
98
|
name: meta.username,
|
|
67
|
-
entityId
|
|
99
|
+
entityId,
|
|
68
100
|
})
|
|
69
101
|
accountId = account.id
|
|
70
|
-
entityId = account.credential
|
|
71
102
|
break
|
|
72
103
|
} catch (err: any) {
|
|
73
104
|
if (err?.code === 11000 || err?.message?.includes('duplicate')) continue
|
|
@@ -75,7 +106,7 @@ export const makeIdentityLinkingService = (): IdentityLinkingService => {
|
|
|
75
106
|
}
|
|
76
107
|
}
|
|
77
108
|
|
|
78
|
-
if (accountId == null
|
|
109
|
+
if (accountId == null) {
|
|
79
110
|
throw new Error('Failed to generate unique account credential after retries')
|
|
80
111
|
}
|
|
81
112
|
|
|
@@ -114,7 +145,7 @@ export const makeIdentityLinkingService = (): IdentityLinkingService => {
|
|
|
114
145
|
role: profile.role,
|
|
115
146
|
userId: profile.userId ?? profile.profileId,
|
|
116
147
|
profileId: profile.profileId,
|
|
117
|
-
|
|
148
|
+
entitySlug: await slugOf(profile.entityId),
|
|
118
149
|
scopes: profile.scopes,
|
|
119
150
|
}
|
|
120
151
|
},
|
|
@@ -145,16 +176,16 @@ export const makeIdentityLinkingService = (): IdentityLinkingService => {
|
|
|
145
176
|
const profileResource = ctx.resource<IdentityProfileResource>(AUTH_IDENTITY_PROFILE)
|
|
146
177
|
const { items: profiles } = await profileResource.list({ criteria: { entityId } })
|
|
147
178
|
|
|
148
|
-
return profiles.map(p => ({
|
|
179
|
+
return await Promise.all(profiles.map(async p => ({
|
|
149
180
|
id: p.profileId,
|
|
150
181
|
name: p.name,
|
|
151
182
|
credential: p.credential,
|
|
152
|
-
|
|
183
|
+
entitySlug: await slugOf(p.entityId),
|
|
153
184
|
scopes: p.scopes,
|
|
154
185
|
groups: p.groups,
|
|
155
186
|
permissions: p.permissions,
|
|
156
187
|
attributes: p.attributes,
|
|
157
|
-
}))
|
|
188
|
+
})))
|
|
158
189
|
},
|
|
159
190
|
|
|
160
191
|
getOwnerCredentials: async (userId: string, entityId?: string, type?: string): Promise<AuthCredentials | undefined> => {
|
|
@@ -182,7 +213,7 @@ export const makeIdentityLinkingService = (): IdentityLinkingService => {
|
|
|
182
213
|
role: profile.role,
|
|
183
214
|
userId: profile.userId ?? profile.profileId,
|
|
184
215
|
profileId: profile.profileId,
|
|
185
|
-
|
|
216
|
+
entitySlug: await slugOf(profile.entityId),
|
|
186
217
|
scopes: profile.scopes,
|
|
187
218
|
challenge: cred.challenge,
|
|
188
219
|
credential: cred.credential,
|
package/src/types.ts
CHANGED
|
@@ -7,23 +7,29 @@ import type { ProviderProfileDetails } from '@owlmeans/oidc'
|
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
9
|
* Local account record — one per user.
|
|
10
|
-
*
|
|
10
|
+
*
|
|
11
|
+
* `id` is the Mongo resource id; `credential` is the account's stable unique key. `entityId`
|
|
12
|
+
* (inherited from Profile below) holds the organization entity's record id — never its slug.
|
|
11
13
|
*/
|
|
12
|
-
export interface IdentityAccount extends Profile, ResourceRecord {
|
|
14
|
+
export interface IdentityAccount extends Omit<Profile, 'entitySlug'>, ResourceRecord {
|
|
13
15
|
id: string
|
|
14
16
|
credential: string
|
|
17
|
+
/** Stable organization-entity id. Renaming the organization does not touch this. */
|
|
18
|
+
entityId?: string
|
|
15
19
|
}
|
|
16
20
|
|
|
17
21
|
/**
|
|
18
22
|
* Local profile record — ties a user to an entity with a role.
|
|
19
23
|
* Mirrors IAMProfile from @owlmeans/auth backend.
|
|
20
24
|
*/
|
|
21
|
-
export interface IdentityProfile extends Profile, ResourceRecord {
|
|
25
|
+
export interface IdentityProfile extends Omit<Profile, 'entitySlug'>, ResourceRecord {
|
|
22
26
|
id: string
|
|
23
27
|
profileId: string
|
|
24
28
|
userId?: string
|
|
25
29
|
role: AuthRole
|
|
26
30
|
expiresAt?: Date
|
|
31
|
+
/** Stable organization-entity id — the value every per-organization query filters on. */
|
|
32
|
+
entityId?: string
|
|
27
33
|
}
|
|
28
34
|
|
|
29
35
|
/**
|
|
@@ -36,6 +42,29 @@ export interface IdentityCredentials extends AuthCredentials, ResourceRecord {
|
|
|
36
42
|
profileId: string
|
|
37
43
|
}
|
|
38
44
|
|
|
45
|
+
/**
|
|
46
|
+
* An organization entity — the record every per-organization store keys on.
|
|
47
|
+
*
|
|
48
|
+
* Its `id` is the stable entity id. The slug is the value users, URLs and tokens see, and it may
|
|
49
|
+
* move; nothing here except `slug` itself changes when it does, which is the entire point of the
|
|
50
|
+
* record existing.
|
|
51
|
+
*/
|
|
52
|
+
export interface OrgEntity extends ResourceRecord {
|
|
53
|
+
id?: string
|
|
54
|
+
slug: string
|
|
55
|
+
formerSlugs?: string[]
|
|
56
|
+
iamKey: string
|
|
57
|
+
/**
|
|
58
|
+
* Names this organization is known by in systems that cannot rename: a Kubernetes namespace, a
|
|
59
|
+
* storage prefix, an IAM realm. Minted once through `mintName` and read back forever after.
|
|
60
|
+
*/
|
|
61
|
+
names?: Record<string, string>
|
|
62
|
+
createdAt: Date
|
|
63
|
+
updatedAt?: Date
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export type OrgEntityResource = MongoResource<OrgEntity>
|
|
67
|
+
|
|
39
68
|
export type IdentityAccountResource = MongoResource<IdentityAccount>
|
|
40
69
|
export type IdentityProfileResource = MongoResource<IdentityProfile>
|
|
41
70
|
export type IdentityCredentialsResource = MongoResource<IdentityCredentials>
|