@owlmeans/server-auth-identity 0.1.3

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 ADDED
@@ -0,0 +1,94 @@
1
+ # @owlmeans/server-auth-identity
2
+
3
+ Mongo-backed local identity resources and provider account linking for OwlMeans server applications.
4
+
5
+ ## Overview
6
+
7
+ - Registers three Mongo resources: local accounts, local profiles, and provider credentials.
8
+ - Provides `IdentityLinkingService` to map external provider profile details into an OwlMeans `AuthPayload`.
9
+ - Keeps external provider identity separate from local product identity and authorization scopes.
10
+ - Designed to work with `@owlmeans/server-auth`, `@owlmeans/server-oidc-rp`, and product-specific module gates.
11
+
12
+ ## Installation
13
+
14
+ ```bash
15
+ bun add @owlmeans/server-auth-identity
16
+ ```
17
+
18
+ ## Usage
19
+
20
+ Register identity resources in a server context after Mongo and auth services are available:
21
+
22
+ ```typescript
23
+ import { appendAuthIdentityResources } from '@owlmeans/server-auth-identity'
24
+
25
+ appendAuthIdentityResources(context)
26
+ ```
27
+
28
+ Link an external provider profile to a local account/profile:
29
+
30
+ ```typescript
31
+ import { AUTH_IDENTITY_LINKING } from '@owlmeans/server-auth-identity'
32
+ import type { IdentityLinkingService } from '@owlmeans/server-auth-identity'
33
+
34
+ const linking = context.service<IdentityLinkingService>(AUTH_IDENTITY_LINKING)
35
+
36
+ const auth = await linking.linkProfile(providerDetails, { username })
37
+ ```
38
+
39
+ Read identity profiles without mutating records:
40
+
41
+ ```typescript
42
+ import { AUTH_IDENTITY_PROFILE } from '@owlmeans/server-auth-identity'
43
+ import type { IdentityProfileResource } from '@owlmeans/server-auth-identity'
44
+
45
+ const profiles = context.resource<IdentityProfileResource>(AUTH_IDENTITY_PROFILE)
46
+ const profile = await profiles.load(profileId, 'profileId')
47
+
48
+ const { items } = await profiles.list({ entityId, profileId } as any)
49
+ ```
50
+
51
+ ## API
52
+
53
+ ### Resource Factories
54
+
55
+ - `makeIdentityAccountResource(dbAlias?)` - local account record, one per user.
56
+ - `makeIdentityProfileResource(dbAlias?)` - local profile record with `entityId`, `role`, `scopes`, and optional expiry.
57
+ - `makeIdentityCredentialsResource(dbAlias?)` - provider credentials link record.
58
+ - `appendAuthIdentityResources(context, dbAlias?)` - registers all resources and `IdentityLinkingService`.
59
+
60
+ ### Aliases
61
+
62
+ - `AUTH_IDENTITY_ACCOUNT` - account resource alias.
63
+ - `AUTH_IDENTITY_PROFILE` - profile resource alias.
64
+ - `AUTH_IDENTITY_CREDENTIALS` - provider credentials resource alias.
65
+ - `AUTH_IDENTITY_LINKING` - linking service alias.
66
+
67
+ ### Types
68
+
69
+ - `IdentityAccount` - `Profile & ResourceRecord`; `credential` is the generated local entity slug.
70
+ - `IdentityProfile` - `Profile & ResourceRecord`; includes `profileId`, `userId?`, `role`, `entityId`, `scopes`, `expiresAt?`.
71
+ - `IdentityCredentials` - `AuthCredentials & ResourceRecord`; includes `profileId` and derived provider keys.
72
+ - `IdentityLinkingService` - `getLinkedProfile`, `linkProfile`, `linkCredentials`, `getOwnerProfiles`, `getOwnerCredentials`.
73
+
74
+ ## Key Derivation
75
+
76
+ - Account `credential`: generated Base58 local entity slug.
77
+ - Profile `profileId`: `"{type}:{accountId}"`.
78
+ - Credentials `userId`: `"{type}:{service}:{providerSub}"`.
79
+ - Credentials `credential`: `"service:{type}:{service}"`.
80
+
81
+ ## Product-Viable Integration Notes
82
+
83
+ - Google/OIDC login is only the provider bootstrap path; durable authorization data lives in `IdentityProfile`.
84
+ - Backend contexts should register `appendAuthService(context)`, then `appendAuthIdentityResources(context)`, then product gate services.
85
+ - Product gates should read `AUTH_IDENTITY_PROFILE`, verify `entityId`, reject expired or blocked profiles, and compare gate params against profile scopes.
86
+ - `Resource.pick()` is destructive and deletes the matching record. Never use `pick()` for gate or handler reads; use `load()` or `list()`.
87
+
88
+ ## Related Packages
89
+
90
+ - [`@owlmeans/auth`](../auth) - core `AuthPayload`, `AuthRole`, `Profile`, and auth errors.
91
+ - [`@owlmeans/server-auth`](../server-auth) - bearer verification and default auth guard.
92
+ - [`@owlmeans/server-oidc-rp`](../server-oidc-rp) - provider exchange and account-linking interface compatibility.
93
+ - [`@owlmeans/oidc`](../oidc) - `ProviderProfileDetails` and provider config types.
94
+ - [`@owlmeans/mongo-resource`](../mongo-resource) - Mongo-backed resource implementation.
@@ -0,0 +1,7 @@
1
+ export declare const AUTH_IDENTITY_ACCOUNT = "auth-identity:account";
2
+ export declare const AUTH_IDENTITY_PROFILE = "auth-identity:profile";
3
+ export declare const AUTH_IDENTITY_CREDENTIALS = "auth-identity:credentials";
4
+ export declare const AUTH_IDENTITY_LINKING = "auth-identity:linking";
5
+ export declare const LOGIN_SERVICE_PREFIX = "service";
6
+ export declare const EXTERNAL_KEY_DELIMITER = ":";
7
+ //# sourceMappingURL=consts.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"consts.d.ts","sourceRoot":"","sources":["../src/consts.ts"],"names":[],"mappings":"AACA,eAAO,MAAM,qBAAqB,0BAA0B,CAAA;AAC5D,eAAO,MAAM,qBAAqB,0BAA0B,CAAA;AAC5D,eAAO,MAAM,yBAAyB,8BAA8B,CAAA;AACpE,eAAO,MAAM,qBAAqB,0BAA0B,CAAA;AAG5D,eAAO,MAAM,oBAAoB,YAAY,CAAA;AAC7C,eAAO,MAAM,sBAAsB,MAAM,CAAA"}
@@ -0,0 +1,8 @@
1
+ export const AUTH_IDENTITY_ACCOUNT = 'auth-identity:account';
2
+ export const AUTH_IDENTITY_PROFILE = 'auth-identity:profile';
3
+ export const AUTH_IDENTITY_CREDENTIALS = 'auth-identity:credentials';
4
+ export const AUTH_IDENTITY_LINKING = 'auth-identity:linking';
5
+ // Helper prefixes for stable key derivation
6
+ export const LOGIN_SERVICE_PREFIX = 'service';
7
+ export const EXTERNAL_KEY_DELIMITER = ':';
8
+ //# sourceMappingURL=consts.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"consts.js","sourceRoot":"","sources":["../src/consts.ts"],"names":[],"mappings":"AACA,MAAM,CAAC,MAAM,qBAAqB,GAAG,uBAAuB,CAAA;AAC5D,MAAM,CAAC,MAAM,qBAAqB,GAAG,uBAAuB,CAAA;AAC5D,MAAM,CAAC,MAAM,yBAAyB,GAAG,2BAA2B,CAAA;AACpE,MAAM,CAAC,MAAM,qBAAqB,GAAG,uBAAuB,CAAA;AAE5D,4CAA4C;AAC5C,MAAM,CAAC,MAAM,oBAAoB,GAAG,SAAS,CAAA;AAC7C,MAAM,CAAC,MAAM,sBAAsB,GAAG,GAAG,CAAA"}
@@ -0,0 +1,3 @@
1
+ import type { BasicContext } from '@owlmeans/context';
2
+ export declare const appendAuthIdentityResources: (context: BasicContext<any>, dbAlias?: string) => void;
3
+ //# sourceMappingURL=helper.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"helper.d.ts","sourceRoot":"","sources":["../src/helper.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAA;AAIrD,eAAO,MAAM,2BAA2B,GACtC,SAAS,YAAY,CAAC,GAAG,CAAC,EAC1B,UAAU,MAAM,KACf,IAKF,CAAA"}
@@ -0,0 +1,9 @@
1
+ import { makeIdentityAccountResource, makeIdentityProfileResource, makeIdentityCredentialsResource } from './resource.js';
2
+ import { makeIdentityLinkingService } from './service.js';
3
+ export const appendAuthIdentityResources = (context, dbAlias) => {
4
+ context.registerResource(makeIdentityAccountResource(dbAlias));
5
+ context.registerResource(makeIdentityProfileResource(dbAlias));
6
+ context.registerResource(makeIdentityCredentialsResource(dbAlias));
7
+ context.registerService(makeIdentityLinkingService());
8
+ };
9
+ //# sourceMappingURL=helper.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"helper.js","sourceRoot":"","sources":["../src/helper.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,2BAA2B,EAAE,2BAA2B,EAAE,+BAA+B,EAAE,MAAM,eAAe,CAAA;AACzH,OAAO,EAAE,0BAA0B,EAAE,MAAM,cAAc,CAAA;AAEzD,MAAM,CAAC,MAAM,2BAA2B,GAAG,CACzC,OAA0B,EAC1B,OAAgB,EACV,EAAE;IACR,OAAO,CAAC,gBAAgB,CAAC,2BAA2B,CAAC,OAAO,CAAC,CAAC,CAAA;IAC9D,OAAO,CAAC,gBAAgB,CAAC,2BAA2B,CAAC,OAAO,CAAC,CAAC,CAAA;IAC9D,OAAO,CAAC,gBAAgB,CAAC,+BAA+B,CAAC,OAAO,CAAC,CAAC,CAAA;IAClE,OAAO,CAAC,eAAe,CAAC,0BAA0B,EAAE,CAAC,CAAA;AACvD,CAAC,CAAA"}
@@ -0,0 +1,6 @@
1
+ export * from './consts.js';
2
+ export type * from './types.js';
3
+ export * from './resource.js';
4
+ export * from './service.js';
5
+ export * from './helper.js';
6
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +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"}
package/build/index.js ADDED
@@ -0,0 +1,5 @@
1
+ export * from './consts.js';
2
+ export * from './resource.js';
3
+ export * from './service.js';
4
+ export * from './helper.js';
5
+ //# sourceMappingURL=index.js.map
@@ -0,0 +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"}
@@ -0,0 +1,5 @@
1
+ import type { IdentityAccountResource, IdentityProfileResource, IdentityCredentialsResource } from './types.js';
2
+ export declare const makeIdentityAccountResource: (dbAlias?: string) => IdentityAccountResource;
3
+ export declare const makeIdentityProfileResource: (dbAlias?: string) => IdentityProfileResource;
4
+ export declare const makeIdentityCredentialsResource: (dbAlias?: string) => IdentityCredentialsResource;
5
+ //# sourceMappingURL=resource.d.ts.map
@@ -0,0 +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;AAGtK,eAAO,MAAM,2BAA2B,GAAI,UAAU,MAAM,KAAG,uBAQ9D,CAAA;AAED,eAAO,MAAM,2BAA2B,GAAI,UAAU,MAAM,KAAG,uBAS9D,CAAA;AAED,eAAO,MAAM,+BAA+B,GAAI,UAAU,MAAM,KAAG,2BAOlE,CAAA"}
@@ -0,0 +1,24 @@
1
+ import { makeMongoResource } from '@owlmeans/mongo-resource';
2
+ import { AUTH_IDENTITY_ACCOUNT, AUTH_IDENTITY_PROFILE, AUTH_IDENTITY_CREDENTIALS } from './consts.js';
3
+ export const makeIdentityAccountResource = (dbAlias) => {
4
+ const resource = makeMongoResource(AUTH_IDENTITY_ACCOUNT, dbAlias);
5
+ resource.index('credential', { credential: 1 }, { unique: true });
6
+ resource.index('entityId', { entityId: 1 });
7
+ resource.index('secret', { secret: 1 }, { unique: true, sparse: true });
8
+ return resource;
9
+ };
10
+ export const makeIdentityProfileResource = (dbAlias) => {
11
+ const resource = makeMongoResource(AUTH_IDENTITY_PROFILE, dbAlias);
12
+ resource.index('userId', { userId: 1 });
13
+ resource.index('entityId', { entityId: 1 });
14
+ resource.index('role', { role: 1, entityId: 1 });
15
+ resource.index('profile', { profileId: 1, entityId: 1 }, { unique: true });
16
+ return resource;
17
+ };
18
+ export const makeIdentityCredentialsResource = (dbAlias) => {
19
+ const resource = makeMongoResource(AUTH_IDENTITY_CREDENTIALS, dbAlias);
20
+ resource.index('provider', { type: 1, userId: 1, credential: 1 }, { unique: true });
21
+ resource.index('profileId', { profileId: 1 });
22
+ return resource;
23
+ };
24
+ //# sourceMappingURL=resource.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resource.js","sourceRoot":"","sources":["../src/resource.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,MAAM,0BAA0B,CAAA;AAE5D,OAAO,EAAE,qBAAqB,EAAE,qBAAqB,EAAE,yBAAyB,EAAE,MAAM,aAAa,CAAA;AAErG,MAAM,CAAC,MAAM,2BAA2B,GAAG,CAAC,OAAgB,EAA2B,EAAE;IACvF,MAAM,QAAQ,GAAG,iBAAiB,CAChC,qBAAqB,EAAE,OAAO,CAC/B,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,CAC/B,CAAA;IACD,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,CACnC,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"}
@@ -0,0 +1,3 @@
1
+ import type { IdentityLinkingService } from './types.js';
2
+ export declare const makeIdentityLinkingService: () => IdentityLinkingService;
3
+ //# sourceMappingURL=service.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"service.d.ts","sourceRoot":"","sources":["../src/service.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAA8F,sBAAsB,EAAE,MAAM,YAAY,CAAA;AAepJ,eAAO,MAAM,0BAA0B,QAAO,sBA6K7C,CAAA"}
@@ -0,0 +1,173 @@
1
+ import { appendContextual } from '@owlmeans/context';
2
+ import { ALL_SCOPES, AuthRole } from '@owlmeans/auth';
3
+ import { createIdOfLength, IdStyle } from '@owlmeans/basic-ids';
4
+ import { AUTH_IDENTITY_ACCOUNT, AUTH_IDENTITY_PROFILE, AUTH_IDENTITY_CREDENTIALS, AUTH_IDENTITY_LINKING, LOGIN_SERVICE_PREFIX, EXTERNAL_KEY_DELIMITER } from './consts.js';
5
+ const MAX_SLUG_RETRIES = 5;
6
+ // Stable external login key: "{type}:{service}:{providerSub}"
7
+ const externalKey = (details) => [details.type, details.service, details.userId].join(EXTERNAL_KEY_DELIMITER);
8
+ // Login-service credential stored on profile and credentials: "service:{type}:{service}"
9
+ const loginService = (details) => [LOGIN_SERVICE_PREFIX, details.type, details.service].join(EXTERNAL_KEY_DELIMITER);
10
+ export const makeIdentityLinkingService = () => {
11
+ const service = appendContextual(AUTH_IDENTITY_LINKING, {
12
+ getLinkedProfile: async (details) => {
13
+ const ctx = service.ctx;
14
+ const credsResource = ctx.resource(AUTH_IDENTITY_CREDENTIALS);
15
+ const { items: creds } = await credsResource.list({
16
+ criteria: {
17
+ type: details.type,
18
+ userId: externalKey(details),
19
+ credential: loginService(details),
20
+ }
21
+ });
22
+ const cred = creds[0] ?? null;
23
+ if (cred == null)
24
+ return null;
25
+ const profileResource = ctx.resource(AUTH_IDENTITY_PROFILE);
26
+ const profile = await profileResource.load(cred.profileId, 'profileId');
27
+ if (profile == null)
28
+ return null;
29
+ return {
30
+ type: details.type,
31
+ role: profile.role,
32
+ userId: profile.userId ?? profile.profileId,
33
+ profileId: profile.profileId,
34
+ entityId: profile.entityId,
35
+ scopes: profile.scopes,
36
+ };
37
+ },
38
+ linkProfile: async (details, meta) => {
39
+ const ctx = service.ctx;
40
+ const accountResource = ctx.resource(AUTH_IDENTITY_ACCOUNT);
41
+ const profileResource = ctx.resource(AUTH_IDENTITY_PROFILE);
42
+ const credsResource = ctx.resource(AUTH_IDENTITY_CREDENTIALS);
43
+ // Generate a unique local entity slug stored as the account credential.
44
+ let accountId = null;
45
+ let entityId = null;
46
+ for (let i = 0; i < MAX_SLUG_RETRIES; i++) {
47
+ const credential = createIdOfLength(16, IdStyle.Base58);
48
+ try {
49
+ const account = await accountResource.create({
50
+ credential,
51
+ name: meta.username,
52
+ entityId: credential,
53
+ });
54
+ accountId = account.id;
55
+ entityId = account.credential;
56
+ break;
57
+ }
58
+ catch (err) {
59
+ if (err?.code === 11000 || err?.message?.includes('duplicate'))
60
+ continue;
61
+ throw err;
62
+ }
63
+ }
64
+ if (accountId == null || entityId == null) {
65
+ throw new Error('Failed to generate unique account credential after retries');
66
+ }
67
+ const profileId = [details.type, accountId].join(EXTERNAL_KEY_DELIMITER);
68
+ const svc = loginService(details);
69
+ let profile;
70
+ try {
71
+ profile = await profileResource.create({
72
+ profileId,
73
+ userId: accountId,
74
+ credential: svc,
75
+ role: AuthRole.User,
76
+ name: meta.username,
77
+ entityId,
78
+ scopes: [ALL_SCOPES],
79
+ });
80
+ }
81
+ catch (err) {
82
+ if (err?.code === 11000 || err?.message?.includes('duplicate')) {
83
+ profile = await profileResource.get(profileId, 'profileId');
84
+ }
85
+ else {
86
+ throw err;
87
+ }
88
+ }
89
+ await credsResource.create({
90
+ challenge: '',
91
+ type: details.type,
92
+ userId: externalKey(details),
93
+ profileId,
94
+ credential: svc,
95
+ });
96
+ return {
97
+ type: details.type,
98
+ role: profile.role,
99
+ userId: profile.userId ?? profile.profileId,
100
+ profileId: profile.profileId,
101
+ entityId: profile.entityId,
102
+ scopes: profile.scopes,
103
+ };
104
+ },
105
+ linkCredentials: async (details) => {
106
+ const result = await service.getLinkedProfile(details);
107
+ if (result == null) {
108
+ throw new Error('Cannot link credentials: profile not found');
109
+ }
110
+ if (details.profileId != null) {
111
+ const ctx = service.ctx;
112
+ const credsResource = ctx.resource(AUTH_IDENTITY_CREDENTIALS);
113
+ await credsResource.create({
114
+ challenge: '',
115
+ type: details.type,
116
+ userId: externalKey(details),
117
+ profileId: details.profileId,
118
+ credential: loginService(details),
119
+ });
120
+ }
121
+ return result;
122
+ },
123
+ getOwnerProfiles: async (entityId) => {
124
+ const ctx = service.ctx;
125
+ const profileResource = ctx.resource(AUTH_IDENTITY_PROFILE);
126
+ const { items: profiles } = await profileResource.list({ criteria: { entityId } });
127
+ return profiles.map(p => ({
128
+ id: p.profileId,
129
+ name: p.name,
130
+ credential: p.credential,
131
+ entityId: p.entityId,
132
+ scopes: p.scopes,
133
+ groups: p.groups,
134
+ permissions: p.permissions,
135
+ attributes: p.attributes,
136
+ }));
137
+ },
138
+ getOwnerCredentials: async (userId, entityId, type) => {
139
+ const ctx = service.ctx;
140
+ const profileResource = ctx.resource(AUTH_IDENTITY_PROFILE);
141
+ const credsResource = ctx.resource(AUTH_IDENTITY_CREDENTIALS);
142
+ const profileFilter = { userId };
143
+ if (entityId != null)
144
+ profileFilter.entityId = entityId;
145
+ const { items: profiles } = await profileResource.list(profileFilter);
146
+ if (profiles.length === 0)
147
+ return undefined;
148
+ const credsFilter = {
149
+ profileId: { $in: profiles.map(p => p.profileId) }
150
+ };
151
+ if (type != null)
152
+ credsFilter.type = type;
153
+ const { items: creds } = await credsResource.list(credsFilter);
154
+ if (creds.length === 0)
155
+ return undefined;
156
+ const cred = creds[0];
157
+ const profile = profiles.find(p => p.profileId === cred.profileId) ?? profiles[0];
158
+ return {
159
+ type: cred.type,
160
+ role: profile.role,
161
+ userId: profile.userId ?? profile.profileId,
162
+ profileId: profile.profileId,
163
+ entityId: profile.entityId,
164
+ scopes: profile.scopes,
165
+ challenge: cred.challenge,
166
+ credential: cred.credential,
167
+ publicKey: cred.publicKey,
168
+ };
169
+ },
170
+ });
171
+ return service;
172
+ };
173
+ //# sourceMappingURL=service.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"service.js","sourceRoot":"","sources":["../src/service.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,66 @@
1
+ import type { ResourceRecord } from '@owlmeans/resource';
2
+ import type { MongoResource } from '@owlmeans/mongo-resource';
3
+ import type { InitializedService } from '@owlmeans/context';
4
+ import type { ServerConfig, ServerContext } from '@owlmeans/server-context';
5
+ import type { AuthCredentials, AuthPayload, AuthRole, Profile } from '@owlmeans/auth';
6
+ import type { ProviderProfileDetails } from '@owlmeans/oidc';
7
+ /**
8
+ * Local account record — one per user.
9
+ * id is the Mongo resource id; credential is the stable unique local entity slug.
10
+ */
11
+ export interface IdentityAccount extends Profile, ResourceRecord {
12
+ id: string;
13
+ credential: string;
14
+ }
15
+ /**
16
+ * Local profile record — ties a user to an entity with a role.
17
+ * Mirrors IAMProfile from @owlmeans/auth backend.
18
+ */
19
+ export interface IdentityProfile extends Profile, ResourceRecord {
20
+ id: string;
21
+ profileId: string;
22
+ userId?: string;
23
+ role: AuthRole;
24
+ expiresAt?: Date;
25
+ }
26
+ /**
27
+ * Provider credentials record — one per provider link per profile.
28
+ * Maps an external provider subject to a local profile using auth-native fields.
29
+ * credential stores the login-service key: "service:{type}:{service}"
30
+ * userId stores the derived external login key: "{type}:{service}:{providerSub}"
31
+ */
32
+ export interface IdentityCredentials extends AuthCredentials, ResourceRecord {
33
+ profileId: string;
34
+ }
35
+ export type IdentityAccountResource = MongoResource<IdentityAccount>;
36
+ export type IdentityProfileResource = MongoResource<IdentityProfile>;
37
+ export type IdentityCredentialsResource = MongoResource<IdentityCredentials>;
38
+ /**
39
+ * Compatible with AccountLinkingService from @owlmeans/server-oidc-rp
40
+ * but defined independently to avoid circular dependency.
41
+ */
42
+ export interface IdentityLinkingService extends InitializedService {
43
+ getLinkedProfile: (details: ProviderProfileDetails) => Promise<AuthPayload | null>;
44
+ linkProfile: (details: ProviderProfileDetails, meta: AccountMeta) => Promise<AuthPayload>;
45
+ linkCredentials: (details: ProviderProfileDetails) => Promise<AuthPayload>;
46
+ getOwnerProfiles: (entityId: string) => Promise<Profile[]>;
47
+ getOwnerCredentials: (userId: string, entityId?: string, type?: string) => Promise<AuthCredentials | undefined>;
48
+ }
49
+ export interface AccountMeta {
50
+ username: string;
51
+ force?: boolean;
52
+ }
53
+ export interface IdentityConfig extends ServerConfig {
54
+ }
55
+ export interface IdentityContext<C extends IdentityConfig = IdentityConfig> extends ServerContext<C> {
56
+ }
57
+ export interface GoogleUserInfo {
58
+ sub: string;
59
+ email: string;
60
+ email_verified?: boolean;
61
+ name?: string;
62
+ given_name?: string;
63
+ family_name?: string;
64
+ picture?: string;
65
+ }
66
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +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;;;GAGG;AACH,MAAM,WAAW,eAAgB,SAAQ,OAAO,EAAE,cAAc;IAC9D,EAAE,EAAE,MAAM,CAAA;IACV,UAAU,EAAE,MAAM,CAAA;CACnB;AAED;;;GAGG;AACH,MAAM,WAAW,eAAgB,SAAQ,OAAO,EAAE,cAAc;IAC9D,EAAE,EAAE,MAAM,CAAA;IACV,SAAS,EAAE,MAAM,CAAA;IACjB,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,IAAI,EAAE,QAAQ,CAAA;IACd,SAAS,CAAC,EAAE,IAAI,CAAA;CACjB;AAED;;;;;GAKG;AACH,MAAM,WAAW,mBAAoB,SAAQ,eAAe,EAAE,cAAc;IAC1E,SAAS,EAAE,MAAM,CAAA;CAClB;AAED,MAAM,MAAM,uBAAuB,GAAG,aAAa,CAAC,eAAe,CAAC,CAAA;AACpE,MAAM,MAAM,uBAAuB,GAAG,aAAa,CAAC,eAAe,CAAC,CAAA;AACpE,MAAM,MAAM,2BAA2B,GAAG,aAAa,CAAC,mBAAmB,CAAC,CAAA;AAE5E;;;GAGG;AACH,MAAM,WAAW,sBAAuB,SAAQ,kBAAkB;IAChE,gBAAgB,EAAE,CAAC,OAAO,EAAE,sBAAsB,KAAK,OAAO,CAAC,WAAW,GAAG,IAAI,CAAC,CAAA;IAClF,WAAW,EAAE,CAAC,OAAO,EAAE,sBAAsB,EAAE,IAAI,EAAE,WAAW,KAAK,OAAO,CAAC,WAAW,CAAC,CAAA;IACzF,eAAe,EAAE,CAAC,OAAO,EAAE,sBAAsB,KAAK,OAAO,CAAC,WAAW,CAAC,CAAA;IAC1E,gBAAgB,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,EAAE,CAAC,CAAA;IAC1D,mBAAmB,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,KAAK,OAAO,CAAC,eAAe,GAAG,SAAS,CAAC,CAAA;CAChH;AAED,MAAM,WAAW,WAAW;IAC1B,QAAQ,EAAE,MAAM,CAAA;IAChB,KAAK,CAAC,EAAE,OAAO,CAAA;CAChB;AAED,MAAM,WAAW,cAAe,SAAQ,YAAY;CACnD;AAED,MAAM,WAAW,eAAe,CAAC,CAAC,SAAS,cAAc,GAAG,cAAc,CAAE,SAAQ,aAAa,CAAC,CAAC,CAAC;CACnG;AAED,MAAM,WAAW,cAAc;IAC7B,GAAG,EAAE,MAAM,CAAA;IACX,KAAK,EAAE,MAAM,CAAA;IACb,cAAc,CAAC,EAAE,OAAO,CAAA;IACxB,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,OAAO,CAAC,EAAE,MAAM,CAAA;CACjB"}
package/build/types.js ADDED
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":""}
package/package.json ADDED
@@ -0,0 +1,45 @@
1
+ {
2
+ "name": "@owlmeans/server-auth-identity",
3
+ "version": "0.1.3",
4
+ "license": "MIT",
5
+ "type": "module",
6
+ "scripts": {
7
+ "build": "tsc -b",
8
+ "dev": "sleep 24 && nodemon -e ts,tsx,json --watch src --exec \"tsc -p ./tsconfig.json\"",
9
+ "watch": "tsc -b -w --preserveWatchOutput --pretty",
10
+ "test": "bun test ./tests"
11
+ },
12
+ "main": "build/index.js",
13
+ "module": "build/index.js",
14
+ "types": "build/index.d.ts",
15
+ "exports": {
16
+ ".": {
17
+ "import": "./build/index.js",
18
+ "require": "./build/index.js",
19
+ "default": "./build/index.js",
20
+ "module": "./build/index.js",
21
+ "types": "./build/index.d.ts"
22
+ }
23
+ },
24
+ "dependencies": {
25
+ "@owlmeans/auth": "^0.1.3",
26
+ "@owlmeans/basic-ids": "^0.1.3",
27
+ "@owlmeans/context": "^0.1.3",
28
+ "@owlmeans/mongo-resource": "^0.1.3",
29
+ "@owlmeans/oidc": "^0.1.3",
30
+ "@owlmeans/resource": "^0.1.3",
31
+ "@owlmeans/server-context": "^0.1.3"
32
+ },
33
+ "peerDependencies": {
34
+ "mongodb": "*"
35
+ },
36
+ "devDependencies": {
37
+ "@owlmeans/dep-config": "workspace:*",
38
+ "@types/bun": "^1.3.0",
39
+ "nodemon": "^3.1.11",
40
+ "typescript": "^6.0.2"
41
+ },
42
+ "publishConfig": {
43
+ "access": "public"
44
+ }
45
+ }
package/src/consts.ts ADDED
@@ -0,0 +1,9 @@
1
+
2
+ export const AUTH_IDENTITY_ACCOUNT = 'auth-identity:account'
3
+ export const AUTH_IDENTITY_PROFILE = 'auth-identity:profile'
4
+ export const AUTH_IDENTITY_CREDENTIALS = 'auth-identity:credentials'
5
+ export const AUTH_IDENTITY_LINKING = 'auth-identity:linking'
6
+
7
+ // Helper prefixes for stable key derivation
8
+ export const LOGIN_SERVICE_PREFIX = 'service'
9
+ export const EXTERNAL_KEY_DELIMITER = ':'
package/src/helper.ts ADDED
@@ -0,0 +1,13 @@
1
+ import type { BasicContext } from '@owlmeans/context'
2
+ import { makeIdentityAccountResource, makeIdentityProfileResource, makeIdentityCredentialsResource } from './resource.js'
3
+ import { makeIdentityLinkingService } from './service.js'
4
+
5
+ export const appendAuthIdentityResources = (
6
+ context: BasicContext<any>,
7
+ dbAlias?: string
8
+ ): void => {
9
+ context.registerResource(makeIdentityAccountResource(dbAlias))
10
+ context.registerResource(makeIdentityProfileResource(dbAlias))
11
+ context.registerResource(makeIdentityCredentialsResource(dbAlias))
12
+ context.registerService(makeIdentityLinkingService())
13
+ }
package/src/index.ts ADDED
@@ -0,0 +1,5 @@
1
+ export * from './consts.js'
2
+ export type * from './types.js'
3
+ export * from './resource.js'
4
+ export * from './service.js'
5
+ export * from './helper.js'
@@ -0,0 +1,33 @@
1
+ import { makeMongoResource } from '@owlmeans/mongo-resource'
2
+ import type { IdentityAccount, IdentityAccountResource, IdentityProfile, IdentityProfileResource, IdentityCredentials, IdentityCredentialsResource } from './types.js'
3
+ import { AUTH_IDENTITY_ACCOUNT, AUTH_IDENTITY_PROFILE, AUTH_IDENTITY_CREDENTIALS } from './consts.js'
4
+
5
+ export const makeIdentityAccountResource = (dbAlias?: string): IdentityAccountResource => {
6
+ const resource = makeMongoResource<IdentityAccount, IdentityAccountResource>(
7
+ AUTH_IDENTITY_ACCOUNT, dbAlias
8
+ )
9
+ resource.index('credential', { credential: 1 }, { unique: true })
10
+ resource.index('entityId', { entityId: 1 })
11
+ resource.index('secret', { secret: 1 }, { unique: true, sparse: true })
12
+ return resource
13
+ }
14
+
15
+ export const makeIdentityProfileResource = (dbAlias?: string): IdentityProfileResource => {
16
+ const resource = makeMongoResource<IdentityProfile, IdentityProfileResource>(
17
+ AUTH_IDENTITY_PROFILE, dbAlias
18
+ )
19
+ resource.index('userId', { userId: 1 })
20
+ resource.index('entityId', { entityId: 1 })
21
+ resource.index('role', { role: 1, entityId: 1 })
22
+ resource.index('profile', { profileId: 1, entityId: 1 }, { unique: true })
23
+ return resource
24
+ }
25
+
26
+ export const makeIdentityCredentialsResource = (dbAlias?: string): IdentityCredentialsResource => {
27
+ const resource = makeMongoResource<IdentityCredentials, IdentityCredentialsResource>(
28
+ AUTH_IDENTITY_CREDENTIALS, dbAlias
29
+ )
30
+ resource.index('provider', { type: 1, userId: 1, credential: 1 }, { unique: true })
31
+ resource.index('profileId', { profileId: 1 })
32
+ return resource
33
+ }
package/src/service.ts ADDED
@@ -0,0 +1,195 @@
1
+ import { appendContextual } from '@owlmeans/context'
2
+ import type { ServerConfig, ServerContext } from '@owlmeans/server-context'
3
+ import type { AuthCredentials, AuthPayload, Profile } from '@owlmeans/auth'
4
+ import { ALL_SCOPES, AuthRole } from '@owlmeans/auth'
5
+ import type { ProviderProfileDetails } from '@owlmeans/oidc'
6
+ import { createIdOfLength, IdStyle } from '@owlmeans/basic-ids'
7
+ import type { AccountMeta, IdentityAccountResource, IdentityProfileResource, IdentityCredentialsResource, IdentityLinkingService } from './types.js'
8
+ import { AUTH_IDENTITY_ACCOUNT, AUTH_IDENTITY_PROFILE, AUTH_IDENTITY_CREDENTIALS, AUTH_IDENTITY_LINKING, LOGIN_SERVICE_PREFIX, EXTERNAL_KEY_DELIMITER } from './consts.js'
9
+
10
+ type Context = ServerContext<ServerConfig>
11
+
12
+ const MAX_SLUG_RETRIES = 5
13
+
14
+ // Stable external login key: "{type}:{service}:{providerSub}"
15
+ const externalKey = (details: ProviderProfileDetails): string =>
16
+ [details.type, details.service, details.userId].join(EXTERNAL_KEY_DELIMITER)
17
+
18
+ // Login-service credential stored on profile and credentials: "service:{type}:{service}"
19
+ const loginService = (details: ProviderProfileDetails): string =>
20
+ [LOGIN_SERVICE_PREFIX, details.type, details.service].join(EXTERNAL_KEY_DELIMITER)
21
+
22
+ export const makeIdentityLinkingService = (): IdentityLinkingService => {
23
+ const service: IdentityLinkingService = appendContextual<IdentityLinkingService>(AUTH_IDENTITY_LINKING, {
24
+ getLinkedProfile: async (details: ProviderProfileDetails): Promise<AuthPayload | null> => {
25
+ const ctx = service.ctx as Context
26
+ const credsResource = ctx.resource<IdentityCredentialsResource>(AUTH_IDENTITY_CREDENTIALS)
27
+
28
+ const { items: creds } = await credsResource.list({
29
+ criteria: {
30
+ type: details.type,
31
+ userId: externalKey(details),
32
+ credential: loginService(details),
33
+ }
34
+ })
35
+ const cred = creds[0] ?? null
36
+ if (cred == null) return null
37
+
38
+ const profileResource = ctx.resource<IdentityProfileResource>(AUTH_IDENTITY_PROFILE)
39
+ const profile = await profileResource.load(cred.profileId, 'profileId')
40
+ if (profile == null) return null
41
+
42
+ return {
43
+ type: details.type,
44
+ role: profile.role,
45
+ userId: profile.userId ?? profile.profileId,
46
+ profileId: profile.profileId,
47
+ entityId: profile.entityId,
48
+ scopes: profile.scopes,
49
+ }
50
+ },
51
+
52
+ linkProfile: async (details: ProviderProfileDetails, meta: AccountMeta): Promise<AuthPayload> => {
53
+ const ctx = service.ctx as Context
54
+ const accountResource = ctx.resource<IdentityAccountResource>(AUTH_IDENTITY_ACCOUNT)
55
+ const profileResource = ctx.resource<IdentityProfileResource>(AUTH_IDENTITY_PROFILE)
56
+ const credsResource = ctx.resource<IdentityCredentialsResource>(AUTH_IDENTITY_CREDENTIALS)
57
+
58
+ // Generate a unique local entity slug stored as the account credential.
59
+ let accountId: string | null = null
60
+ let entityId: string | null = null
61
+ for (let i = 0; i < MAX_SLUG_RETRIES; i++) {
62
+ const credential = createIdOfLength(16, IdStyle.Base58)
63
+ try {
64
+ const account = await accountResource.create({
65
+ credential,
66
+ name: meta.username,
67
+ entityId: credential,
68
+ })
69
+ accountId = account.id
70
+ entityId = account.credential
71
+ break
72
+ } catch (err: any) {
73
+ if (err?.code === 11000 || err?.message?.includes('duplicate')) continue
74
+ throw err
75
+ }
76
+ }
77
+
78
+ if (accountId == null || entityId == null) {
79
+ throw new Error('Failed to generate unique account credential after retries')
80
+ }
81
+
82
+ const profileId = [details.type, accountId].join(EXTERNAL_KEY_DELIMITER)
83
+ const svc = loginService(details)
84
+
85
+ let profile
86
+ try {
87
+ profile = await profileResource.create({
88
+ profileId,
89
+ userId: accountId,
90
+ credential: svc,
91
+ role: AuthRole.User,
92
+ name: meta.username,
93
+ entityId,
94
+ scopes: [ALL_SCOPES],
95
+ })
96
+ } catch (err: any) {
97
+ if (err?.code === 11000 || err?.message?.includes('duplicate')) {
98
+ profile = await profileResource.get(profileId, 'profileId')
99
+ } else {
100
+ throw err
101
+ }
102
+ }
103
+
104
+ await credsResource.create({
105
+ challenge: '',
106
+ type: details.type,
107
+ userId: externalKey(details),
108
+ profileId,
109
+ credential: svc,
110
+ })
111
+
112
+ return {
113
+ type: details.type,
114
+ role: profile.role,
115
+ userId: profile.userId ?? profile.profileId,
116
+ profileId: profile.profileId,
117
+ entityId: profile.entityId,
118
+ scopes: profile.scopes,
119
+ }
120
+ },
121
+
122
+ linkCredentials: async (details: ProviderProfileDetails): Promise<AuthPayload> => {
123
+ const result = await service.getLinkedProfile(details)
124
+ if (result == null) {
125
+ throw new Error('Cannot link credentials: profile not found')
126
+ }
127
+
128
+ if (details.profileId != null) {
129
+ const ctx = service.ctx as Context
130
+ const credsResource = ctx.resource<IdentityCredentialsResource>(AUTH_IDENTITY_CREDENTIALS)
131
+ await credsResource.create({
132
+ challenge: '',
133
+ type: details.type,
134
+ userId: externalKey(details),
135
+ profileId: details.profileId,
136
+ credential: loginService(details),
137
+ })
138
+ }
139
+
140
+ return result
141
+ },
142
+
143
+ getOwnerProfiles: async (entityId: string): Promise<Profile[]> => {
144
+ const ctx = service.ctx as Context
145
+ const profileResource = ctx.resource<IdentityProfileResource>(AUTH_IDENTITY_PROFILE)
146
+ const { items: profiles } = await profileResource.list({ criteria: { entityId } })
147
+
148
+ return profiles.map(p => ({
149
+ id: p.profileId,
150
+ name: p.name,
151
+ credential: p.credential,
152
+ entityId: p.entityId,
153
+ scopes: p.scopes,
154
+ groups: p.groups,
155
+ permissions: p.permissions,
156
+ attributes: p.attributes,
157
+ }))
158
+ },
159
+
160
+ getOwnerCredentials: async (userId: string, entityId?: string, type?: string): Promise<AuthCredentials | undefined> => {
161
+ const ctx = service.ctx as Context
162
+ const profileResource = ctx.resource<IdentityProfileResource>(AUTH_IDENTITY_PROFILE)
163
+ const credsResource = ctx.resource<IdentityCredentialsResource>(AUTH_IDENTITY_CREDENTIALS)
164
+
165
+ const profileFilter: Record<string, unknown> = { userId }
166
+ if (entityId != null) profileFilter.entityId = entityId
167
+ const { items: profiles } = await profileResource.list(profileFilter as any)
168
+ if (profiles.length === 0) return undefined
169
+
170
+ const credsFilter: Record<string, unknown> = {
171
+ profileId: { $in: profiles.map(p => p.profileId) }
172
+ }
173
+ if (type != null) credsFilter.type = type
174
+ const { items: creds } = await credsResource.list(credsFilter as any)
175
+ if (creds.length === 0) return undefined
176
+
177
+ const cred = creds[0]
178
+ const profile = profiles.find(p => p.profileId === cred.profileId) ?? profiles[0]
179
+
180
+ return {
181
+ type: cred.type,
182
+ role: profile.role,
183
+ userId: profile.userId ?? profile.profileId,
184
+ profileId: profile.profileId,
185
+ entityId: profile.entityId,
186
+ scopes: profile.scopes,
187
+ challenge: cred.challenge,
188
+ credential: cred.credential,
189
+ publicKey: cred.publicKey,
190
+ }
191
+ },
192
+ })
193
+
194
+ return service
195
+ }
package/src/types.ts ADDED
@@ -0,0 +1,75 @@
1
+ import type { ResourceRecord } from '@owlmeans/resource'
2
+ import type { MongoResource } from '@owlmeans/mongo-resource'
3
+ import type { InitializedService } from '@owlmeans/context'
4
+ import type { ServerConfig, ServerContext } from '@owlmeans/server-context'
5
+ import type { AuthCredentials, AuthPayload, AuthRole, Profile } from '@owlmeans/auth'
6
+ import type { ProviderProfileDetails } from '@owlmeans/oidc'
7
+
8
+ /**
9
+ * Local account record — one per user.
10
+ * id is the Mongo resource id; credential is the stable unique local entity slug.
11
+ */
12
+ export interface IdentityAccount extends Profile, ResourceRecord {
13
+ id: string
14
+ credential: string
15
+ }
16
+
17
+ /**
18
+ * Local profile record — ties a user to an entity with a role.
19
+ * Mirrors IAMProfile from @owlmeans/auth backend.
20
+ */
21
+ export interface IdentityProfile extends Profile, ResourceRecord {
22
+ id: string
23
+ profileId: string
24
+ userId?: string
25
+ role: AuthRole
26
+ expiresAt?: Date
27
+ }
28
+
29
+ /**
30
+ * Provider credentials record — one per provider link per profile.
31
+ * Maps an external provider subject to a local profile using auth-native fields.
32
+ * credential stores the login-service key: "service:{type}:{service}"
33
+ * userId stores the derived external login key: "{type}:{service}:{providerSub}"
34
+ */
35
+ export interface IdentityCredentials extends AuthCredentials, ResourceRecord {
36
+ profileId: string
37
+ }
38
+
39
+ export type IdentityAccountResource = MongoResource<IdentityAccount>
40
+ export type IdentityProfileResource = MongoResource<IdentityProfile>
41
+ export type IdentityCredentialsResource = MongoResource<IdentityCredentials>
42
+
43
+ /**
44
+ * Compatible with AccountLinkingService from @owlmeans/server-oidc-rp
45
+ * but defined independently to avoid circular dependency.
46
+ */
47
+ export interface IdentityLinkingService extends InitializedService {
48
+ getLinkedProfile: (details: ProviderProfileDetails) => Promise<AuthPayload | null>
49
+ linkProfile: (details: ProviderProfileDetails, meta: AccountMeta) => Promise<AuthPayload>
50
+ linkCredentials: (details: ProviderProfileDetails) => Promise<AuthPayload>
51
+ getOwnerProfiles: (entityId: string) => Promise<Profile[]>
52
+ getOwnerCredentials: (userId: string, entityId?: string, type?: string) => Promise<AuthCredentials | undefined>
53
+ }
54
+
55
+ export interface AccountMeta {
56
+ username: string
57
+ force?: boolean
58
+ }
59
+
60
+ export interface IdentityConfig extends ServerConfig {
61
+ }
62
+
63
+ export interface IdentityContext<C extends IdentityConfig = IdentityConfig> extends ServerContext<C> {
64
+ }
65
+
66
+ export interface GoogleUserInfo {
67
+ sub: string
68
+ email: string
69
+ email_verified?: boolean
70
+ name?: string
71
+ given_name?: string
72
+ family_name?: string
73
+ picture?: string
74
+ }
75
+
package/tsconfig.json ADDED
@@ -0,0 +1,8 @@
1
+ {
2
+ "extends": ["@owlmeans/dep-config/tsconfig.base.json", "@owlmeans/dep-config/tsconfig.server.json"],
3
+ "compilerOptions": {
4
+ "rootDir": "./src",
5
+ "outDir": "./build/"
6
+ },
7
+ "include": ["src"]
8
+ }