@owlmeans/server-auth 0.1.10 → 0.1.14

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.
@@ -7,7 +7,7 @@ applyTo: "**/*.ts, **/*.tsx"
7
7
  # @owlmeans/server-auth
8
8
 
9
9
  **Layer:** Server
10
- **Install:** `"@owlmeans/server-auth": "^0.1.10"` in `dependencies`
10
+ **Install:** `"@owlmeans/server-auth": "^0.1.14"` in `dependencies`
11
11
 
12
12
  ## Key Exports
13
13
 
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
3
  "package": "@owlmeans/server-auth",
4
- "version": "0.1.10",
5
- "generatedAt": "2026-06-17T09:37:28.169Z",
4
+ "version": "0.1.14",
5
+ "generatedAt": "2026-08-05T16:56:53.374Z",
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
9
9
 
10
10
  **Layer:** Server
11
- **Install:** `"@owlmeans/server-auth": "^0.1.10"` in `dependencies`
11
+ **Install:** `"@owlmeans/server-auth": "^0.1.14"` in `dependencies`
12
12
 
13
13
  ## Key Exports
14
14
 
@@ -8,4 +8,5 @@ export * from './consts.js';
8
8
  export * from './rely/index.js';
9
9
  export * from './actions/index.js';
10
10
  export * from './plugins/index.js';
11
+ export * from './supervisor.js';
11
12
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/manager/index.ts"],"names":[],"mappings":"AAAA,mBAAmB,YAAY,CAAA;AAE/B,cAAc,cAAc,CAAA;AAC5B,cAAc,cAAc,CAAA;AAC5B,cAAc,cAAc,CAAA;AAC5B,cAAc,WAAW,CAAA;AACzB,cAAc,WAAW,CAAA;AACzB,cAAc,aAAa,CAAA;AAC3B,cAAc,iBAAiB,CAAA;AAC/B,cAAc,oBAAoB,CAAA;AAClC,cAAc,oBAAoB,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/manager/index.ts"],"names":[],"mappings":"AAAA,mBAAmB,YAAY,CAAA;AAE/B,cAAc,cAAc,CAAA;AAC5B,cAAc,cAAc,CAAA;AAC5B,cAAc,cAAc,CAAA;AAC5B,cAAc,WAAW,CAAA;AACzB,cAAc,WAAW,CAAA;AACzB,cAAc,aAAa,CAAA;AAC3B,cAAc,iBAAiB,CAAA;AAC/B,cAAc,oBAAoB,CAAA;AAClC,cAAc,oBAAoB,CAAA;AAClC,cAAc,iBAAiB,CAAA"}
@@ -7,4 +7,5 @@ export * from './consts.js';
7
7
  export * from './rely/index.js';
8
8
  export * from './actions/index.js';
9
9
  export * from './plugins/index.js';
10
+ export * from './supervisor.js';
10
11
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/manager/index.ts"],"names":[],"mappings":"AAEA,cAAc,cAAc,CAAA;AAC5B,cAAc,cAAc,CAAA;AAC5B,cAAc,cAAc,CAAA;AAC5B,cAAc,WAAW,CAAA;AACzB,cAAc,WAAW,CAAA;AACzB,cAAc,aAAa,CAAA;AAC3B,cAAc,iBAAiB,CAAA;AAC/B,cAAc,oBAAoB,CAAA;AAClC,cAAc,oBAAoB,CAAA"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/manager/index.ts"],"names":[],"mappings":"AAEA,cAAc,cAAc,CAAA;AAC5B,cAAc,cAAc,CAAA;AAC5B,cAAc,cAAc,CAAA;AAC5B,cAAc,WAAW,CAAA;AACzB,cAAc,WAAW,CAAA;AACzB,cAAc,aAAa,CAAA;AAC3B,cAAc,iBAAiB,CAAA;AAC/B,cAAc,oBAAoB,CAAA;AAClC,cAAc,oBAAoB,CAAA;AAClC,cAAc,iBAAiB,CAAA"}
@@ -2,6 +2,7 @@ export type * from './types.js';
2
2
  export * from './basic-ed25519.js';
3
3
  export * from './re-captcha.js';
4
4
  export * from './basic-rely.js';
5
+ export * from './supervisor.js';
5
6
  export * from './utils.js';
6
7
  export { plugins, registerPlugin } from './index.js';
7
8
  //# sourceMappingURL=export.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"export.d.ts","sourceRoot":"","sources":["../../../src/manager/plugins/export.ts"],"names":[],"mappings":"AACA,mBAAmB,YAAY,CAAA;AAC/B,cAAc,oBAAoB,CAAA;AAClC,cAAc,iBAAiB,CAAA;AAC/B,cAAc,iBAAiB,CAAA;AAC/B,cAAc,YAAY,CAAA;AAC1B,OAAO,EAAE,OAAO,EAAE,cAAc,EAAE,MAAM,YAAY,CAAA"}
1
+ {"version":3,"file":"export.d.ts","sourceRoot":"","sources":["../../../src/manager/plugins/export.ts"],"names":[],"mappings":"AACA,mBAAmB,YAAY,CAAA;AAC/B,cAAc,oBAAoB,CAAA;AAClC,cAAc,iBAAiB,CAAA;AAC/B,cAAc,iBAAiB,CAAA;AAC/B,cAAc,iBAAiB,CAAA;AAC/B,cAAc,YAAY,CAAA;AAC1B,OAAO,EAAE,OAAO,EAAE,cAAc,EAAE,MAAM,YAAY,CAAA"}
@@ -1,6 +1,7 @@
1
1
  export * from './basic-ed25519.js';
2
2
  export * from './re-captcha.js';
3
3
  export * from './basic-rely.js';
4
+ export * from './supervisor.js';
4
5
  export * from './utils.js';
5
6
  export { plugins, registerPlugin } from './index.js';
6
7
  //# sourceMappingURL=export.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"export.js","sourceRoot":"","sources":["../../../src/manager/plugins/export.ts"],"names":[],"mappings":"AAEA,cAAc,oBAAoB,CAAA;AAClC,cAAc,iBAAiB,CAAA;AAC/B,cAAc,iBAAiB,CAAA;AAC/B,cAAc,YAAY,CAAA;AAC1B,OAAO,EAAE,OAAO,EAAE,cAAc,EAAE,MAAM,YAAY,CAAA"}
1
+ {"version":3,"file":"export.js","sourceRoot":"","sources":["../../../src/manager/plugins/export.ts"],"names":[],"mappings":"AAEA,cAAc,oBAAoB,CAAA;AAClC,cAAc,iBAAiB,CAAA;AAC/B,cAAc,iBAAiB,CAAA;AAC/B,cAAc,iBAAiB,CAAA;AAC/B,cAAc,YAAY,CAAA;AAC1B,OAAO,EAAE,OAAO,EAAE,cAAc,EAAE,MAAM,YAAY,CAAA"}
@@ -0,0 +1,18 @@
1
+ import type { AuthPlugin } from './types.js';
2
+ import type { AppConfig, AppContext } from '../types.js';
3
+ import type { SupervisorPluginOptions } from '../supervisor.js';
4
+ /**
5
+ * PK-based supervisor authentication plugin. A privileged operator (or an e2e
6
+ * test) holds one of the project's trusted private keys (the allowlisted
7
+ * `supervisors`) and signs `buildSupervisorPayload` to mint a token for an
8
+ * arbitrary user id / email - bypassing external IdPs. Development-only by
9
+ * design (see `appendSupervisorAuth`).
10
+ *
11
+ * The plugin issues NO token itself: it only verifies the supervisor signature
12
+ * and resolves/registers the target user, then hands the mutated credential back
13
+ * to the existing `makeAuthModel` flow which signs the credential envelope, and
14
+ * to the project's auth service which exchanges it for the final
15
+ * `Ed25519BasicToken` bearer.
16
+ */
17
+ export declare const makeSupervisorPlugin: (context: AppContext<AppConfig>, opts: SupervisorPluginOptions) => AuthPlugin;
18
+ //# sourceMappingURL=supervisor.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"supervisor.d.ts","sourceRoot":"","sources":["../../../src/manager/plugins/supervisor.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAA;AAM5C,OAAO,KAAK,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,aAAa,CAAA;AACxD,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,kBAAkB,CAAA;AAE/D;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,oBAAoB,GAC/B,SAAS,UAAU,CAAC,SAAS,CAAC,EAAE,MAAM,uBAAuB,KAC5D,UA8DF,CAAA"}
@@ -0,0 +1,75 @@
1
+ import { AuthenticationType, AuthenFailed, AuthRole, ALL_SCOPES, buildSupervisorPayload } from '@owlmeans/auth';
2
+ import { base64 } from '@scure/base';
3
+ import { randomBytes } from '@noble/hashes/utils';
4
+ import { fromPubKey } from '@owlmeans/basic-keys';
5
+ import { TRUSTED } from '@owlmeans/config';
6
+ /**
7
+ * PK-based supervisor authentication plugin. A privileged operator (or an e2e
8
+ * test) holds one of the project's trusted private keys (the allowlisted
9
+ * `supervisors`) and signs `buildSupervisorPayload` to mint a token for an
10
+ * arbitrary user id / email - bypassing external IdPs. Development-only by
11
+ * design (see `appendSupervisorAuth`).
12
+ *
13
+ * The plugin issues NO token itself: it only verifies the supervisor signature
14
+ * and resolves/registers the target user, then hands the mutated credential back
15
+ * to the existing `makeAuthModel` flow which signs the credential envelope, and
16
+ * to the project's auth service which exchanges it for the final
17
+ * `Ed25519BasicToken` bearer.
18
+ */
19
+ export const makeSupervisorPlugin = (context, opts) => {
20
+ const plugin = {
21
+ type: AuthenticationType.Supervisor,
22
+ init: async () => ({ challenge: base64.encode(randomBytes(32)) }),
23
+ authenticate: async (credential) => {
24
+ // `credential.challenge` has already been replaced by the model with the
25
+ // unwrapped, single-use server challenge (`msg`).
26
+ let parsed;
27
+ try {
28
+ parsed = JSON.parse(credential.credential);
29
+ }
30
+ catch {
31
+ throw new AuthenFailed('supervisor:credential');
32
+ }
33
+ if (parsed?.signature == null || parsed?.salt == null || credential.userId == null) {
34
+ throw new AuthenFailed('supervisor:payload');
35
+ }
36
+ const payload = buildSupervisorPayload(credential.challenge, credential.userId, parsed.salt);
37
+ let matched = null;
38
+ for (const alias of opts.supervisors) {
39
+ try {
40
+ const record = await context.getConfigResource(TRUSTED)
41
+ .load(alias, 'name');
42
+ if (record?.credential == null) {
43
+ continue;
44
+ }
45
+ if (await fromPubKey(record.credential).verify(payload, parsed.signature)) {
46
+ matched = alias;
47
+ break;
48
+ }
49
+ }
50
+ catch {
51
+ // unknown trusted alias or verification error - try the next supervisor
52
+ }
53
+ }
54
+ if (matched == null) {
55
+ throw new AuthenFailed('supervisor:signature');
56
+ }
57
+ const resolution = opts.resolveUser != null
58
+ ? await opts.resolveUser(credential.userId, context, { register: opts.allowRegistration })
59
+ : { userId: credential.userId };
60
+ credential.userId = resolution.userId;
61
+ credential.profileId = resolution.profileId ?? resolution.userId;
62
+ if (resolution.entityId != null) {
63
+ credential.entityId = resolution.entityId;
64
+ }
65
+ credential.scopes = resolution.scopes ?? credential.scopes ?? [ALL_SCOPES];
66
+ credential.role = resolution.role ?? AuthRole.User;
67
+ const token = base64.encode(randomBytes(32));
68
+ credential.challenge = token;
69
+ credential.type = AuthenticationType.OneTimeToken;
70
+ return { token };
71
+ }
72
+ };
73
+ return plugin;
74
+ };
75
+ //# sourceMappingURL=supervisor.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"supervisor.js","sourceRoot":"","sources":["../../../src/manager/plugins/supervisor.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,63 @@
1
+ import type { AuthPayload } from '@owlmeans/auth';
2
+ import type { AppConfig, AppContext } from './types.js';
3
+ /**
4
+ * What a `SupervisorUserResolver` returns: the identity the supervisor-minted
5
+ * token will represent. Only `userId` is required - the rest default sensibly.
6
+ */
7
+ export interface SupervisorUserResolution extends Partial<Pick<AuthPayload, 'profileId' | 'entityId' | 'role' | 'scopes'>> {
8
+ userId: string;
9
+ }
10
+ /**
11
+ * Find-or-create the target identity for a supervisor login. `register` reflects
12
+ * `allowRegistration`; when false the resolver should only look existing users up.
13
+ * Wire this to the project's identity store (e.g. `@owlmeans/server-auth-identity`).
14
+ */
15
+ export interface SupervisorUserResolver {
16
+ <C extends AppConfig, T extends AppContext<C>>(userId: string, context: T, opts: {
17
+ register: boolean;
18
+ }): Promise<SupervisorUserResolution>;
19
+ }
20
+ /** Resolved options handed to the plugin factory. */
21
+ export interface SupervisorPluginOptions {
22
+ supervisors: string[];
23
+ allowRegistration: boolean;
24
+ resolveUser?: SupervisorUserResolver;
25
+ }
26
+ export interface SupervisorAuthOptions {
27
+ /** Trusted-record aliases authorized to act as supervisor. Default: master + superuser. */
28
+ supervisors?: string[];
29
+ /** Find-or-create the target user by id/email. Default: trust the id as-is. */
30
+ resolveUser?: SupervisorUserResolver;
31
+ /** Allow minting a token for an unknown user (registration). Default: true. */
32
+ allowRegistration?: boolean;
33
+ /** Force enable/disable. Default: development only (cfg.debug.all || cfg.debug.supervisor). */
34
+ enabled?: boolean;
35
+ /**
36
+ * Also accept internal owlmeans `Ed25519BasicToken`s even when another guard
37
+ * (e.g. OIDC) is the primary guard on protected modules. Default: true.
38
+ */
39
+ acceptInternalTokens?: boolean;
40
+ /** The internal-token guard to add as a coguard. Default: DEFAULT_GUARD ('auth'). */
41
+ guard?: string;
42
+ }
43
+ /**
44
+ * Ensure the internal `Ed25519BasicToken` guard is accepted as a coguard on every
45
+ * already-guarded backend module, so internal owlmeans tokens keep working even
46
+ * when another guard (e.g. OIDC) is the primary guard. The primary guard stays
47
+ * first; the internal guard is appended as a fallback (its `match` only fires for
48
+ * an `Ed25519BasicToken` authorization header).
49
+ */
50
+ export declare const setupInternalTokenCoguard: (modules: Array<{
51
+ guards?: string[];
52
+ }>, guard?: string) => void;
53
+ /**
54
+ * Unified, explicit append for PK-based supervisor authentication. Call it once
55
+ * on the auth-manager server context. Development-only by default.
56
+ *
57
+ * It registers the supervisor auth plugin (verifies a front-end signature against
58
+ * the allowlisted trusted keys, then resolves/registers the target user) and -
59
+ * unless disabled - makes protected modules also accept internal owlmeans tokens
60
+ * (requirement: understand internal tokens even when OIDC is the primary guard).
61
+ */
62
+ export declare const appendSupervisorAuth: <C extends AppConfig, T extends AppContext<C>>(context: T, opts?: SupervisorAuthOptions) => T;
63
+ //# sourceMappingURL=supervisor.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"supervisor.d.ts","sourceRoot":"","sources":["../../src/manager/supervisor.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAA;AAEjD,OAAO,KAAK,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,YAAY,CAAA;AAIvD;;;GAGG;AACH,MAAM,WAAW,wBAAyB,SAAQ,OAAO,CAAC,IAAI,CAAC,WAAW,EACxE,WAAW,GAAG,UAAU,GAAG,MAAM,GAAG,QAAQ,CAAC,CAAC;IAC9C,MAAM,EAAE,MAAM,CAAA;CACf;AAED;;;;GAIG;AACH,MAAM,WAAW,sBAAsB;IACrC,CAAC,CAAC,SAAS,SAAS,EAAE,CAAC,SAAS,UAAU,CAAC,CAAC,CAAC,EAC3C,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE;QAAE,QAAQ,EAAE,OAAO,CAAA;KAAE,GACtD,OAAO,CAAC,wBAAwB,CAAC,CAAA;CACrC;AAED,qDAAqD;AACrD,MAAM,WAAW,uBAAuB;IACtC,WAAW,EAAE,MAAM,EAAE,CAAA;IACrB,iBAAiB,EAAE,OAAO,CAAA;IAC1B,WAAW,CAAC,EAAE,sBAAsB,CAAA;CACrC;AAED,MAAM,WAAW,qBAAqB;IACpC,2FAA2F;IAC3F,WAAW,CAAC,EAAE,MAAM,EAAE,CAAA;IACtB,+EAA+E;IAC/E,WAAW,CAAC,EAAE,sBAAsB,CAAA;IACpC,+EAA+E;IAC/E,iBAAiB,CAAC,EAAE,OAAO,CAAA;IAC3B,+FAA+F;IAC/F,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB;;;OAGG;IACH,oBAAoB,CAAC,EAAE,OAAO,CAAA;IAC9B,qFAAqF;IACrF,KAAK,CAAC,EAAE,MAAM,CAAA;CACf;AAOD;;;;;;GAMG;AACH,eAAO,MAAM,yBAAyB,GACpC,SAAS,KAAK,CAAC;IAAE,MAAM,CAAC,EAAE,MAAM,EAAE,CAAA;CAAE,CAAC,EAAE,QAAO,MAAsB,KACnE,IAMF,CAAA;AAED;;;;;;;;GAQG;AACH,eAAO,MAAM,oBAAoB,GAAI,CAAC,SAAS,SAAS,EAAE,CAAC,SAAS,UAAU,CAAC,CAAC,CAAC,EAC/E,SAAS,CAAC,EAAE,OAAO,qBAAqB,KACvC,CAyBF,CAAA"}
@@ -0,0 +1,46 @@
1
+ import { AuthenticationType } from '@owlmeans/auth';
2
+ import { DEFAULT_GUARD } from '@owlmeans/auth-common';
3
+ import { registerPlugin } from './plugins/index.js';
4
+ import { makeSupervisorPlugin } from './plugins/supervisor.js';
5
+ const DEFAULT_SUPERVISORS = ['master', 'superuser'];
6
+ const isDevelopment = (context) => context.cfg.debug?.all === true || context.cfg.debug?.supervisor === true;
7
+ /**
8
+ * Ensure the internal `Ed25519BasicToken` guard is accepted as a coguard on every
9
+ * already-guarded backend module, so internal owlmeans tokens keep working even
10
+ * when another guard (e.g. OIDC) is the primary guard. The primary guard stays
11
+ * first; the internal guard is appended as a fallback (its `match` only fires for
12
+ * an `Ed25519BasicToken` authorization header).
13
+ */
14
+ export const setupInternalTokenCoguard = (modules, guard = DEFAULT_GUARD) => {
15
+ modules.forEach(module => {
16
+ if (module.guards != null && module.guards.length > 0 && !module.guards.includes(guard)) {
17
+ module.guards.push(guard);
18
+ }
19
+ });
20
+ };
21
+ /**
22
+ * Unified, explicit append for PK-based supervisor authentication. Call it once
23
+ * on the auth-manager server context. Development-only by default.
24
+ *
25
+ * It registers the supervisor auth plugin (verifies a front-end signature against
26
+ * the allowlisted trusted keys, then resolves/registers the target user) and -
27
+ * unless disabled - makes protected modules also accept internal owlmeans tokens
28
+ * (requirement: understand internal tokens even when OIDC is the primary guard).
29
+ */
30
+ export const appendSupervisorAuth = (context, opts) => {
31
+ const enabled = opts?.enabled ?? isDevelopment(context);
32
+ if (!enabled) {
33
+ return context;
34
+ }
35
+ const resolved = {
36
+ supervisors: opts?.supervisors ?? DEFAULT_SUPERVISORS,
37
+ allowRegistration: opts?.allowRegistration ?? true,
38
+ resolveUser: opts?.resolveUser
39
+ };
40
+ registerPlugin(AuthenticationType.Supervisor, (ctx => makeSupervisorPlugin(ctx, resolved)));
41
+ if (opts?.acceptInternalTokens !== false) {
42
+ setupInternalTokenCoguard(context.entrypoints(), opts?.guard ?? DEFAULT_GUARD);
43
+ }
44
+ return context;
45
+ };
46
+ //# sourceMappingURL=supervisor.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"supervisor.js","sourceRoot":"","sources":["../../src/manager/supervisor.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAE,MAAM,gBAAgB,CAAA;AAEnD,OAAO,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAA;AAErD,OAAO,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAA;AACnD,OAAO,EAAE,oBAAoB,EAAE,MAAM,yBAAyB,CAAA;AA+C9D,MAAM,mBAAmB,GAAG,CAAC,QAAQ,EAAE,WAAW,CAAC,CAAA;AAEnD,MAAM,aAAa,GAAG,CAAC,OAAqE,EAAW,EAAE,CACvG,OAAO,CAAC,GAAG,CAAC,KAAK,EAAE,GAAG,KAAK,IAAI,IAAI,OAAO,CAAC,GAAG,CAAC,KAAK,EAAE,UAAU,KAAK,IAAI,CAAA;AAE3E;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,yBAAyB,GAAG,CACvC,OAAqC,EAAE,QAAgB,aAAa,EAC9D,EAAE;IACR,OAAO,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE;QACvB,IAAI,MAAM,CAAC,MAAM,IAAI,IAAI,IAAI,MAAM,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,EAAE,CAAC;YACxF,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAA;QAC3B,CAAC;IACH,CAAC,CAAC,CAAA;AACJ,CAAC,CAAA;AAED;;;;;;;;GAQG;AACH,MAAM,CAAC,MAAM,oBAAoB,GAAG,CAClC,OAAU,EAAE,IAA4B,EACrC,EAAE;IACL,MAAM,OAAO,GAAG,IAAI,EAAE,OAAO,IAAI,aAAa,CAAC,OAAO,CAAC,CAAA;IACvD,IAAI,CAAC,OAAO,EAAE,CAAC;QACb,OAAO,OAAO,CAAA;IAChB,CAAC;IAED,MAAM,QAAQ,GAA4B;QACxC,WAAW,EAAE,IAAI,EAAE,WAAW,IAAI,mBAAmB;QACrD,iBAAiB,EAAE,IAAI,EAAE,iBAAiB,IAAI,IAAI;QAClD,WAAW,EAAE,IAAI,EAAE,WAAW;KAC/B,CAAA;IAED,cAAc,CACZ,kBAAkB,CAAC,UAAU,EAC7B,CAAC,GAAG,CAAC,EAAE,CAAC,oBAAoB,CAAC,GAAuC,EAAE,QAAQ,CAAC,CAAyC,CACzH,CAAA;IAED,IAAI,IAAI,EAAE,oBAAoB,KAAK,KAAK,EAAE,CAAC;QACzC,yBAAyB,CACvB,OAAO,CAAC,WAAW,EAA6C,EAChE,IAAI,EAAE,KAAK,IAAI,aAAa,CAC7B,CAAA;IACH,CAAC;IAED,OAAO,OAAO,CAAA;AAChB,CAAC,CAAA"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@owlmeans/server-auth",
3
- "version": "0.1.10",
3
+ "version": "0.1.14",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "scripts": {
@@ -37,29 +37,29 @@
37
37
  },
38
38
  "dependencies": {
39
39
  "@noble/hashes": "^1.5.0",
40
- "@owlmeans/api": "^0.1.10",
41
- "@owlmeans/api-config-server": "^0.1.10",
42
- "@owlmeans/auth": "^0.1.10",
43
- "@owlmeans/auth-common": "^0.1.10",
44
- "@owlmeans/basic-envelope": "^0.1.10",
45
- "@owlmeans/basic-ids": "^0.1.10",
46
- "@owlmeans/basic-keys": "^0.1.10",
47
- "@owlmeans/client-config": "^0.1.10",
48
- "@owlmeans/client-entrypoint": "^0.1.10",
49
- "@owlmeans/config": "^0.1.10",
50
- "@owlmeans/context": "^0.1.10",
51
- "@owlmeans/kluster": "^0.1.10",
52
- "@owlmeans/entrypoint": "^0.1.10",
53
- "@owlmeans/redis-resource": "^0.1.10",
54
- "@owlmeans/resource": "^0.1.10",
55
- "@owlmeans/route": "^0.1.10",
56
- "@owlmeans/server-api": "^0.1.10",
57
- "@owlmeans/server-context": "^0.1.10",
58
- "@owlmeans/server-entrypoint": "^0.1.10",
59
- "@owlmeans/server-route": "^0.1.10",
60
- "@owlmeans/server-socket": "^0.1.10",
61
- "@owlmeans/socket": "^0.1.10",
62
- "@owlmeans/static-resource": "^0.1.10",
40
+ "@owlmeans/api": "^0.1.14",
41
+ "@owlmeans/api-config-server": "^0.1.14",
42
+ "@owlmeans/auth": "^0.1.14",
43
+ "@owlmeans/auth-common": "^0.1.14",
44
+ "@owlmeans/basic-envelope": "^0.1.14",
45
+ "@owlmeans/basic-ids": "^0.1.14",
46
+ "@owlmeans/basic-keys": "^0.1.14",
47
+ "@owlmeans/client-config": "^0.1.14",
48
+ "@owlmeans/client-entrypoint": "^0.1.14",
49
+ "@owlmeans/config": "^0.1.14",
50
+ "@owlmeans/context": "^0.1.14",
51
+ "@owlmeans/kluster": "^0.1.14",
52
+ "@owlmeans/entrypoint": "^0.1.14",
53
+ "@owlmeans/redis-resource": "^0.1.14",
54
+ "@owlmeans/resource": "^0.1.14",
55
+ "@owlmeans/route": "^0.1.14",
56
+ "@owlmeans/server-api": "^0.1.14",
57
+ "@owlmeans/server-context": "^0.1.14",
58
+ "@owlmeans/server-entrypoint": "^0.1.14",
59
+ "@owlmeans/server-route": "^0.1.14",
60
+ "@owlmeans/server-socket": "^0.1.14",
61
+ "@owlmeans/socket": "^0.1.14",
62
+ "@owlmeans/static-resource": "^0.1.14",
63
63
  "@scure/base": "^1.1.9"
64
64
  },
65
65
  "peerDependencies": {
@@ -67,12 +67,11 @@
67
67
  },
68
68
  "devDependencies": {
69
69
  "@owlmeans/dep-config": "workspace:*",
70
- "@owlmeans/test-auth": "^0.1.10",
71
- "@types/bun": "^1.3.0",
72
- "@types/node": "^24.10.1",
73
- "nodemon": "^3.1.11",
74
- "npm-check": "^6.0.1",
75
- "typescript": "^6.0.2"
70
+ "@owlmeans/test-auth": "^0.1.14",
71
+ "@types/bun": "^1.3.14",
72
+ "@types/node": "^26.1.0",
73
+ "nodemon": "^3.1.14",
74
+ "typescript": "^6.0.3"
76
75
  },
77
76
  "publishConfig": {
78
77
  "access": "public"
@@ -9,3 +9,4 @@ export * from './consts.js'
9
9
  export * from './rely/index.js'
10
10
  export * from './actions/index.js'
11
11
  export * from './plugins/index.js'
12
+ export * from './supervisor.js'
@@ -3,5 +3,6 @@ export type * from './types.js'
3
3
  export * from './basic-ed25519.js'
4
4
  export * from './re-captcha.js'
5
5
  export * from './basic-rely.js'
6
+ export * from './supervisor.js'
6
7
  export * from './utils.js'
7
8
  export { plugins, registerPlugin } from './index.js'
@@ -0,0 +1,89 @@
1
+ import { AuthenticationType, AuthenFailed, AuthRole, ALL_SCOPES, buildSupervisorPayload } from '@owlmeans/auth'
2
+ import type { SupervisorCredentialPayload } from '@owlmeans/auth'
3
+ import type { AuthPlugin } from './types.js'
4
+ import { base64 } from '@scure/base'
5
+ import { randomBytes } from '@noble/hashes/utils'
6
+ import { fromPubKey } from '@owlmeans/basic-keys'
7
+ import { TRUSTED } from '@owlmeans/config'
8
+ import type { TrustedRecord } from '@owlmeans/auth-common'
9
+ import type { AppConfig, AppContext } from '../types.js'
10
+ import type { SupervisorPluginOptions } from '../supervisor.js'
11
+
12
+ /**
13
+ * PK-based supervisor authentication plugin. A privileged operator (or an e2e
14
+ * test) holds one of the project's trusted private keys (the allowlisted
15
+ * `supervisors`) and signs `buildSupervisorPayload` to mint a token for an
16
+ * arbitrary user id / email - bypassing external IdPs. Development-only by
17
+ * design (see `appendSupervisorAuth`).
18
+ *
19
+ * The plugin issues NO token itself: it only verifies the supervisor signature
20
+ * and resolves/registers the target user, then hands the mutated credential back
21
+ * to the existing `makeAuthModel` flow which signs the credential envelope, and
22
+ * to the project's auth service which exchanges it for the final
23
+ * `Ed25519BasicToken` bearer.
24
+ */
25
+ export const makeSupervisorPlugin = (
26
+ context: AppContext<AppConfig>, opts: SupervisorPluginOptions
27
+ ): AuthPlugin => {
28
+ const plugin: AuthPlugin = {
29
+ type: AuthenticationType.Supervisor,
30
+
31
+ init: async () => ({ challenge: base64.encode(randomBytes(32)) }),
32
+
33
+ authenticate: async credential => {
34
+ // `credential.challenge` has already been replaced by the model with the
35
+ // unwrapped, single-use server challenge (`msg`).
36
+ let parsed: SupervisorCredentialPayload
37
+ try {
38
+ parsed = JSON.parse(credential.credential) as SupervisorCredentialPayload
39
+ } catch {
40
+ throw new AuthenFailed('supervisor:credential')
41
+ }
42
+ if (parsed?.signature == null || parsed?.salt == null || credential.userId == null) {
43
+ throw new AuthenFailed('supervisor:payload')
44
+ }
45
+
46
+ const payload = buildSupervisorPayload(credential.challenge, credential.userId, parsed.salt)
47
+
48
+ let matched: string | null = null
49
+ for (const alias of opts.supervisors) {
50
+ try {
51
+ const record = await context.getConfigResource(TRUSTED)
52
+ .load<TrustedRecord>(alias, 'name')
53
+ if (record?.credential == null) {
54
+ continue
55
+ }
56
+ if (await fromPubKey(record.credential).verify(payload, parsed.signature)) {
57
+ matched = alias
58
+ break
59
+ }
60
+ } catch {
61
+ // unknown trusted alias or verification error - try the next supervisor
62
+ }
63
+ }
64
+ if (matched == null) {
65
+ throw new AuthenFailed('supervisor:signature')
66
+ }
67
+
68
+ const resolution = opts.resolveUser != null
69
+ ? await opts.resolveUser(credential.userId, context, { register: opts.allowRegistration })
70
+ : { userId: credential.userId }
71
+
72
+ credential.userId = resolution.userId
73
+ credential.profileId = resolution.profileId ?? resolution.userId
74
+ if (resolution.entityId != null) {
75
+ credential.entityId = resolution.entityId
76
+ }
77
+ credential.scopes = resolution.scopes ?? credential.scopes ?? [ALL_SCOPES]
78
+ credential.role = resolution.role ?? AuthRole.User
79
+
80
+ const token = base64.encode(randomBytes(32))
81
+ credential.challenge = token
82
+ credential.type = AuthenticationType.OneTimeToken
83
+
84
+ return { token }
85
+ }
86
+ }
87
+
88
+ return plugin
89
+ }
@@ -0,0 +1,111 @@
1
+ import { AuthenticationType } from '@owlmeans/auth'
2
+ import type { AuthPayload } from '@owlmeans/auth'
3
+ import { DEFAULT_GUARD } from '@owlmeans/auth-common'
4
+ import type { AppConfig, AppContext } from './types.js'
5
+ import { registerPlugin } from './plugins/index.js'
6
+ import { makeSupervisorPlugin } from './plugins/supervisor.js'
7
+
8
+ /**
9
+ * What a `SupervisorUserResolver` returns: the identity the supervisor-minted
10
+ * token will represent. Only `userId` is required - the rest default sensibly.
11
+ */
12
+ export interface SupervisorUserResolution extends Partial<Pick<AuthPayload,
13
+ 'profileId' | 'entityId' | 'role' | 'scopes'>> {
14
+ userId: string
15
+ }
16
+
17
+ /**
18
+ * Find-or-create the target identity for a supervisor login. `register` reflects
19
+ * `allowRegistration`; when false the resolver should only look existing users up.
20
+ * Wire this to the project's identity store (e.g. `@owlmeans/server-auth-identity`).
21
+ */
22
+ export interface SupervisorUserResolver {
23
+ <C extends AppConfig, T extends AppContext<C>>(
24
+ userId: string, context: T, opts: { register: boolean }
25
+ ): Promise<SupervisorUserResolution>
26
+ }
27
+
28
+ /** Resolved options handed to the plugin factory. */
29
+ export interface SupervisorPluginOptions {
30
+ supervisors: string[]
31
+ allowRegistration: boolean
32
+ resolveUser?: SupervisorUserResolver
33
+ }
34
+
35
+ export interface SupervisorAuthOptions {
36
+ /** Trusted-record aliases authorized to act as supervisor. Default: master + superuser. */
37
+ supervisors?: string[]
38
+ /** Find-or-create the target user by id/email. Default: trust the id as-is. */
39
+ resolveUser?: SupervisorUserResolver
40
+ /** Allow minting a token for an unknown user (registration). Default: true. */
41
+ allowRegistration?: boolean
42
+ /** Force enable/disable. Default: development only (cfg.debug.all || cfg.debug.supervisor). */
43
+ enabled?: boolean
44
+ /**
45
+ * Also accept internal owlmeans `Ed25519BasicToken`s even when another guard
46
+ * (e.g. OIDC) is the primary guard on protected modules. Default: true.
47
+ */
48
+ acceptInternalTokens?: boolean
49
+ /** The internal-token guard to add as a coguard. Default: DEFAULT_GUARD ('auth'). */
50
+ guard?: string
51
+ }
52
+
53
+ const DEFAULT_SUPERVISORS = ['master', 'superuser']
54
+
55
+ const isDevelopment = (context: { cfg: { debug?: { all?: boolean, supervisor?: boolean } } }): boolean =>
56
+ context.cfg.debug?.all === true || context.cfg.debug?.supervisor === true
57
+
58
+ /**
59
+ * Ensure the internal `Ed25519BasicToken` guard is accepted as a coguard on every
60
+ * already-guarded backend module, so internal owlmeans tokens keep working even
61
+ * when another guard (e.g. OIDC) is the primary guard. The primary guard stays
62
+ * first; the internal guard is appended as a fallback (its `match` only fires for
63
+ * an `Ed25519BasicToken` authorization header).
64
+ */
65
+ export const setupInternalTokenCoguard = (
66
+ modules: Array<{ guards?: string[] }>, guard: string = DEFAULT_GUARD
67
+ ): void => {
68
+ modules.forEach(module => {
69
+ if (module.guards != null && module.guards.length > 0 && !module.guards.includes(guard)) {
70
+ module.guards.push(guard)
71
+ }
72
+ })
73
+ }
74
+
75
+ /**
76
+ * Unified, explicit append for PK-based supervisor authentication. Call it once
77
+ * on the auth-manager server context. Development-only by default.
78
+ *
79
+ * It registers the supervisor auth plugin (verifies a front-end signature against
80
+ * the allowlisted trusted keys, then resolves/registers the target user) and -
81
+ * unless disabled - makes protected modules also accept internal owlmeans tokens
82
+ * (requirement: understand internal tokens even when OIDC is the primary guard).
83
+ */
84
+ export const appendSupervisorAuth = <C extends AppConfig, T extends AppContext<C>>(
85
+ context: T, opts?: SupervisorAuthOptions
86
+ ): T => {
87
+ const enabled = opts?.enabled ?? isDevelopment(context)
88
+ if (!enabled) {
89
+ return context
90
+ }
91
+
92
+ const resolved: SupervisorPluginOptions = {
93
+ supervisors: opts?.supervisors ?? DEFAULT_SUPERVISORS,
94
+ allowRegistration: opts?.allowRegistration ?? true,
95
+ resolveUser: opts?.resolveUser
96
+ }
97
+
98
+ registerPlugin(
99
+ AuthenticationType.Supervisor,
100
+ (ctx => makeSupervisorPlugin(ctx as unknown as AppContext<AppConfig>, resolved)) as Parameters<typeof registerPlugin>[1]
101
+ )
102
+
103
+ if (opts?.acceptInternalTokens !== false) {
104
+ setupInternalTokenCoguard(
105
+ context.entrypoints() as unknown as Array<{ guards?: string[] }>,
106
+ opts?.guard ?? DEFAULT_GUARD
107
+ )
108
+ }
109
+
110
+ return context
111
+ }
@@ -0,0 +1,108 @@
1
+ import { describe, test, expect } from 'bun:test'
2
+ import { makeFixtureKeyPair } from '@owlmeans/test-auth'
3
+ import { AuthenticationType, AuthRole, buildSupervisorPayload } from '@owlmeans/auth'
4
+ import type { AuthCredentials } from '@owlmeans/auth'
5
+ import type { TrustedRecord } from '@owlmeans/auth-common'
6
+ import { makeSupervisorPlugin } from '../src/manager/plugins/supervisor.js'
7
+ import type { AppConfig, AppContext } from '../src/manager/types.js'
8
+
9
+ const SUPERVISOR = 'master'
10
+
11
+ const makeStubContext = (records: TrustedRecord[]): AppContext<AppConfig> => ({
12
+ getConfigResource: () => ({
13
+ load: async (id: string, field?: string) =>
14
+ records.find(r => (field === 'name' ? r.name === id : r.id === id)) ?? null
15
+ })
16
+ } as unknown as AppContext<AppConfig>)
17
+
18
+ const signCredential = async (
19
+ kp: ReturnType<typeof makeFixtureKeyPair>, challenge: string, userId: string, salt: string
20
+ ): Promise<string> => {
21
+ const signature = await kp.sign(buildSupervisorPayload(challenge, userId, salt))
22
+ return JSON.stringify({ salt, signature })
23
+ }
24
+
25
+ const baseCredential = (challenge: string, userId: string, credential: string): AuthCredentials => ({
26
+ type: AuthenticationType.Supervisor,
27
+ challenge,
28
+ credential,
29
+ userId,
30
+ role: AuthRole.User,
31
+ scopes: ['*'],
32
+ })
33
+
34
+ describe('@owlmeans/server-auth — supervisor plugin', () => {
35
+ test('verifies an allowlisted supervisor signature and issues a one-time token', async () => {
36
+ const kp = makeFixtureKeyPair(SUPERVISOR)
37
+ const record: TrustedRecord = { id: kp.exportAddress(), name: SUPERVISOR, credential: kp.exportPublic(), scopes: ['*'] }
38
+ const context = makeStubContext([record])
39
+
40
+ const plugin = makeSupervisorPlugin(context, { supervisors: [SUPERVISOR], allowRegistration: true })
41
+
42
+ const challenge = 'challenge-' + Date.now()
43
+ const userId = 'newuser@example.com'
44
+ const credential = baseCredential(challenge, userId, await signCredential(kp, challenge, userId, 'salt-1'))
45
+
46
+ const result = await plugin.authenticate(credential)
47
+
48
+ expect(result.token).toBeString()
49
+ expect(result.token.length).toBeGreaterThan(0)
50
+ expect(credential.type).toBe(AuthenticationType.OneTimeToken)
51
+ expect(credential.challenge).toBe(result.token)
52
+ expect(credential.userId).toBe(userId)
53
+ expect(credential.role).toBe(AuthRole.User)
54
+ })
55
+
56
+ test('applies the resolveUser mapping (registration / impersonation)', async () => {
57
+ const kp = makeFixtureKeyPair(SUPERVISOR)
58
+ const record: TrustedRecord = { id: kp.exportAddress(), name: SUPERVISOR, credential: kp.exportPublic(), scopes: ['*'] }
59
+ const context = makeStubContext([record])
60
+
61
+ const plugin = makeSupervisorPlugin(context, {
62
+ supervisors: [SUPERVISOR],
63
+ allowRegistration: true,
64
+ resolveUser: async (userId) => ({ userId: 'internal-id', profileId: 'profile-1', entityId: 'entity-1', scopes: ['project'] })
65
+ })
66
+
67
+ const challenge = 'challenge-' + Date.now()
68
+ const userId = 'someone@example.com'
69
+ const credential = baseCredential(challenge, userId, await signCredential(kp, challenge, userId, 'salt-2'))
70
+
71
+ await plugin.authenticate(credential)
72
+
73
+ expect(credential.userId).toBe('internal-id')
74
+ expect(credential.profileId).toBe('profile-1')
75
+ expect(credential.entityId).toBe('entity-1')
76
+ expect(credential.scopes).toEqual(['project'])
77
+ })
78
+
79
+ test('rejects a signature from a non-allowlisted key', async () => {
80
+ const supervisorKp = makeFixtureKeyPair(SUPERVISOR)
81
+ const attackerKp = makeFixtureKeyPair('attacker')
82
+ const record: TrustedRecord = { id: supervisorKp.exportAddress(), name: SUPERVISOR, credential: supervisorKp.exportPublic(), scopes: ['*'] }
83
+ const context = makeStubContext([record])
84
+
85
+ const plugin = makeSupervisorPlugin(context, { supervisors: [SUPERVISOR], allowRegistration: true })
86
+
87
+ const challenge = 'challenge-' + Date.now()
88
+ const userId = 'victim@example.com'
89
+ const credential = baseCredential(challenge, userId, await signCredential(attackerKp, challenge, userId, 'salt-3'))
90
+
91
+ await expect(plugin.authenticate(credential)).rejects.toThrow()
92
+ })
93
+
94
+ test('rejects a signature bound to a different challenge (replay protection)', async () => {
95
+ const kp = makeFixtureKeyPair(SUPERVISOR)
96
+ const record: TrustedRecord = { id: kp.exportAddress(), name: SUPERVISOR, credential: kp.exportPublic(), scopes: ['*'] }
97
+ const context = makeStubContext([record])
98
+
99
+ const plugin = makeSupervisorPlugin(context, { supervisors: [SUPERVISOR], allowRegistration: true })
100
+
101
+ const userId = 'user@example.com'
102
+ // signature made for one challenge, presented with another
103
+ const credentialStr = await signCredential(kp, 'original-challenge', userId, 'salt-4')
104
+ const credential = baseCredential('different-challenge', userId, credentialStr)
105
+
106
+ await expect(plugin.authenticate(credential)).rejects.toThrow()
107
+ })
108
+ })