@owlmeans/test-auth 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 +13 -0
- package/build/envelope.d.ts +16 -0
- package/build/envelope.d.ts.map +1 -0
- package/build/envelope.js +25 -0
- package/build/envelope.js.map +1 -0
- package/build/fixtures.d.ts +5 -0
- package/build/fixtures.d.ts.map +1 -0
- package/build/fixtures.js +14 -0
- package/build/fixtures.js.map +1 -0
- package/build/guard.d.ts +30 -0
- package/build/guard.d.ts.map +1 -0
- package/build/guard.js +40 -0
- package/build/guard.js.map +1 -0
- package/build/index.d.ts +6 -0
- package/build/index.d.ts.map +1 -0
- package/build/index.js +6 -0
- package/build/index.js.map +1 -0
- package/build/keys.d.ts +10 -0
- package/build/keys.d.ts.map +1 -0
- package/build/keys.js +19 -0
- package/build/keys.js.map +1 -0
- package/build/trusted.d.ts +14 -0
- package/build/trusted.d.ts.map +1 -0
- package/build/trusted.js +67 -0
- package/build/trusted.js.map +1 -0
- package/package.json +42 -0
- package/src/envelope.ts +42 -0
- package/src/fixtures.ts +16 -0
- package/src/guard.ts +63 -0
- package/src/index.ts +5 -0
- package/src/keys.ts +21 -0
- package/src/trusted.ts +81 -0
- package/tsconfig.json +11 -0
package/README.md
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# @owlmeans/test-auth
|
|
2
|
+
|
|
3
|
+
The **only** OwlMeans package that ships authentication/authorization mocks. Tests in any other package may import from here when they need a fake authenticated identity, a deterministic Ed25519 keypair, or an in-memory `TRUSTED` resource. No other mocks (database, network, sibling-package services) belong in test code — those packages get integration tests instead. See the `auth-protocol` skill for the protocol this mock implements.
|
|
4
|
+
|
|
5
|
+
Helpers exported here:
|
|
6
|
+
|
|
7
|
+
- `makeFixtureKeyPair(seed?)` — deterministic Ed25519 `KeyPairModel`.
|
|
8
|
+
- `makeMemoryTrustedResource(records?)` — `Resource<TrustedRecord>` satisfying `trust()` lookups against the `TRUSTED` config resource.
|
|
9
|
+
- `makeMockGuard({ alias?, auth?, allow? })` — `GuardService` that resolves to a chosen `Auth`. Implements `match`, `handle`, `authenticated`.
|
|
10
|
+
- `withAuth(ctx, auth)` — convenience that registers a mock guard with a chosen `Auth` on the context.
|
|
11
|
+
- `signMockEnvelope(msg, type, kind?, kp?)` — wraps `makeEnvelopeModel` with a fixture keypair to produce a signed envelope.
|
|
12
|
+
- `makeBearer(auth, kp?)` — `ED25519-BASIC-TOKEN <encoded>` header value for unit tests of header parsing.
|
|
13
|
+
- Canonical fixtures: `SUPERUSER`, `USER`, `SERVICE` `Auth` payloads.
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { EnvelopeKind } from '@owlmeans/basic-envelope';
|
|
2
|
+
import type { KeyPairModel } from '@owlmeans/basic-keys';
|
|
3
|
+
import type { Auth } from '@owlmeans/auth';
|
|
4
|
+
/**
|
|
5
|
+
* Wrap a payload in a signed `EnvelopeModel` using a fixture keypair (or one
|
|
6
|
+
* supplied by the caller) and serialize it. Useful in category-B tests that
|
|
7
|
+
* exercise envelope unwrapping/verification without hitting a real signer.
|
|
8
|
+
*/
|
|
9
|
+
export declare const signMockEnvelope: <T extends {} | string>(msg: T, type: string, kind?: EnvelopeKind, kp?: KeyPairModel) => Promise<string>;
|
|
10
|
+
/**
|
|
11
|
+
* Produce an `Authorization` header value of the form
|
|
12
|
+
* `ED25519-BASIC-TOKEN <encoded>` for a given `Auth`. Lets unit tests of
|
|
13
|
+
* header parsing and guard `match` logic generate valid-looking bearers.
|
|
14
|
+
*/
|
|
15
|
+
export declare const makeBearer: (auth: Auth, kp?: KeyPairModel) => Promise<string>;
|
|
16
|
+
//# sourceMappingURL=envelope.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"envelope.d.ts","sourceRoot":"","sources":["../src/envelope.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAqB,MAAM,0BAA0B,CAAA;AAC1E,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAA;AAExD,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,gBAAgB,CAAA;AAK1C;;;;GAIG;AACH,eAAO,MAAM,gBAAgB,GAAU,CAAC,SAAS,EAAE,GAAG,MAAM,EAC1D,KAAK,CAAC,EACN,MAAM,MAAM,EACZ,OAAM,YAAiC,EACvC,KAAI,YAAsD,KACzD,OAAO,CAAC,MAAM,CAKhB,CAAA;AAED;;;;GAIG;AACH,eAAO,MAAM,UAAU,GACrB,MAAM,IAAI,EACV,KAAK,YAAY,KAChB,OAAO,CAAC,MAAM,CAQhB,CAAA"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { EnvelopeKind, makeEnvelopeModel } from '@owlmeans/basic-envelope';
|
|
2
|
+
import { AuthroizationType } from '@owlmeans/auth';
|
|
3
|
+
import { makeFixtureKeyPair } from './keys.js';
|
|
4
|
+
const AUTH_BEARER_PREFIX = AuthroizationType.Ed25519BasicToken.toUpperCase();
|
|
5
|
+
/**
|
|
6
|
+
* Wrap a payload in a signed `EnvelopeModel` using a fixture keypair (or one
|
|
7
|
+
* supplied by the caller) and serialize it. Useful in category-B tests that
|
|
8
|
+
* exercise envelope unwrapping/verification without hitting a real signer.
|
|
9
|
+
*/
|
|
10
|
+
export const signMockEnvelope = async (msg, type, kind = EnvelopeKind.Token, kp = makeFixtureKeyPair('test-auth-default')) => {
|
|
11
|
+
const envelope = makeEnvelopeModel(type);
|
|
12
|
+
envelope.send(msg);
|
|
13
|
+
const signed = await envelope.sign(kp, kind);
|
|
14
|
+
return signed;
|
|
15
|
+
};
|
|
16
|
+
/**
|
|
17
|
+
* Produce an `Authorization` header value of the form
|
|
18
|
+
* `ED25519-BASIC-TOKEN <encoded>` for a given `Auth`. Lets unit tests of
|
|
19
|
+
* header parsing and guard `match` logic generate valid-looking bearers.
|
|
20
|
+
*/
|
|
21
|
+
export const makeBearer = async (auth, kp) => {
|
|
22
|
+
const encoded = await signMockEnvelope(auth, AuthroizationType.Ed25519BasicToken, EnvelopeKind.Token, kp);
|
|
23
|
+
return `${AUTH_BEARER_PREFIX} ${encoded}`;
|
|
24
|
+
};
|
|
25
|
+
//# sourceMappingURL=envelope.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"envelope.js","sourceRoot":"","sources":["../src/envelope.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,iBAAiB,EAAE,MAAM,0BAA0B,CAAA;AAE1E,OAAO,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAA;AAElD,OAAO,EAAE,kBAAkB,EAAE,MAAM,WAAW,CAAA;AAE9C,MAAM,kBAAkB,GAAG,iBAAiB,CAAC,iBAAiB,CAAC,WAAW,EAAE,CAAA;AAE5E;;;;GAIG;AACH,MAAM,CAAC,MAAM,gBAAgB,GAAG,KAAK,EACnC,GAAM,EACN,IAAY,EACZ,OAAqB,YAAY,CAAC,KAAK,EACvC,KAAmB,kBAAkB,CAAC,mBAAmB,CAAC,EACzC,EAAE;IACnB,MAAM,QAAQ,GAAG,iBAAiB,CAAI,IAAI,CAAC,CAAA;IAC3C,QAAQ,CAAC,IAAI,CAAC,GAAG,CAAC,CAAA;IAClB,MAAM,MAAM,GAAG,MAAM,QAAQ,CAAC,IAAI,CAAC,EAAE,EAAE,IAAI,CAAC,CAAA;IAC5C,OAAO,MAAM,CAAA;AACf,CAAC,CAAA;AAED;;;;GAIG;AACH,MAAM,CAAC,MAAM,UAAU,GAAG,KAAK,EAC7B,IAAU,EACV,EAAiB,EACA,EAAE;IACnB,MAAM,OAAO,GAAG,MAAM,gBAAgB,CACpC,IAA0C,EAC1C,iBAAiB,CAAC,iBAAiB,EACnC,YAAY,CAAC,KAAK,EAClB,EAAE,CACH,CAAA;IACD,OAAO,GAAG,kBAAkB,IAAI,OAAO,EAAE,CAAA;AAC3C,CAAC,CAAA"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fixtures.d.ts","sourceRoot":"","sources":["../src/fixtures.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,gBAAgB,CAAA;AAY1C,eAAO,MAAM,SAAS,EAAE,IAAqE,CAAA;AAC7F,eAAO,MAAM,IAAI,EAAE,IAAsD,CAAA;AACzE,eAAO,MAAM,OAAO,EAAE,IAA+D,CAAA"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { AuthRole, AuthenticationType } from '@owlmeans/auth';
|
|
2
|
+
const baseAuth = (role, userId, token) => ({
|
|
3
|
+
type: AuthenticationType.BasicEd25519,
|
|
4
|
+
role,
|
|
5
|
+
userId,
|
|
6
|
+
token,
|
|
7
|
+
isUser: role !== AuthRole.Service && role !== AuthRole.System,
|
|
8
|
+
createdAt: new Date(0),
|
|
9
|
+
scopes: ['*'],
|
|
10
|
+
});
|
|
11
|
+
export const SUPERUSER = baseAuth(AuthRole.Superuser, 'superuser-1', 'token-superuser');
|
|
12
|
+
export const USER = baseAuth(AuthRole.User, 'user-1', 'token-user');
|
|
13
|
+
export const SERVICE = baseAuth(AuthRole.Service, 'service-1', 'token-service');
|
|
14
|
+
//# sourceMappingURL=fixtures.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fixtures.js","sourceRoot":"","sources":["../src/fixtures.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,kBAAkB,EAAE,MAAM,gBAAgB,CAAA;AAG7D,MAAM,QAAQ,GAAG,CAAC,IAAc,EAAE,MAAc,EAAE,KAAa,EAAQ,EAAE,CAAC,CAAC;IACzE,IAAI,EAAE,kBAAkB,CAAC,YAAY;IACrC,IAAI;IACJ,MAAM;IACN,KAAK;IACL,MAAM,EAAE,IAAI,KAAK,QAAQ,CAAC,OAAO,IAAI,IAAI,KAAK,QAAQ,CAAC,MAAM;IAC7D,SAAS,EAAE,IAAI,IAAI,CAAC,CAAC,CAAC;IACtB,MAAM,EAAE,CAAC,GAAG,CAAC;CACd,CAAC,CAAA;AAEF,MAAM,CAAC,MAAM,SAAS,GAAS,QAAQ,CAAC,QAAQ,CAAC,SAAS,EAAE,aAAa,EAAE,iBAAiB,CAAC,CAAA;AAC7F,MAAM,CAAC,MAAM,IAAI,GAAS,QAAQ,CAAC,QAAQ,CAAC,IAAI,EAAE,QAAQ,EAAE,YAAY,CAAC,CAAA;AACzE,MAAM,CAAC,MAAM,OAAO,GAAS,QAAQ,CAAC,QAAQ,CAAC,OAAO,EAAE,WAAW,EAAE,eAAe,CAAC,CAAA"}
|
package/build/guard.d.ts
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { BasicConfig, BasicContext } from '@owlmeans/context';
|
|
2
|
+
import type { Auth } from '@owlmeans/auth';
|
|
3
|
+
import type { AbstractRequest, AbstractResponse, GuardService } from '@owlmeans/entrypoint';
|
|
4
|
+
export interface MockGuardOptions {
|
|
5
|
+
alias?: string;
|
|
6
|
+
/** Auth payload that `handle` will resolve into the response. */
|
|
7
|
+
auth?: Auth;
|
|
8
|
+
/** Optional predicate that decides whether `match` reports a hit. Defaults to "always match". */
|
|
9
|
+
allow?: (req: AbstractRequest, res: AbstractResponse<unknown>) => boolean | Promise<boolean>;
|
|
10
|
+
/** Token returned by the client-side `authenticated()` method. */
|
|
11
|
+
token?: string;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Build a `GuardService` that resolves to a chosen `Auth` without exercising
|
|
15
|
+
* any real cryptography or trusted-record lookup. Use this in category-B
|
|
16
|
+
* tests where the goal is to verify behaviour that depends on an
|
|
17
|
+
* already-authenticated identity.
|
|
18
|
+
*
|
|
19
|
+
* The mock implements the full `GuardService` shape — `match`, `handle`,
|
|
20
|
+
* `authenticated` — so it can be registered on a context exactly the way
|
|
21
|
+
* a real guard is.
|
|
22
|
+
*/
|
|
23
|
+
export declare const makeMockGuard: (opts?: MockGuardOptions) => GuardService;
|
|
24
|
+
/**
|
|
25
|
+
* Register a mock guard on `context` that resolves to the given `auth`.
|
|
26
|
+
* Returns the same context for chaining. Existing guards under the same
|
|
27
|
+
* alias are not replaced — register before any real guard service.
|
|
28
|
+
*/
|
|
29
|
+
export declare const withAuth: <C extends BasicConfig, T extends BasicContext<C>>(context: T, auth: Auth, alias?: string) => T;
|
|
30
|
+
//# sourceMappingURL=guard.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"guard.d.ts","sourceRoot":"","sources":["../src/guard.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAA;AAElE,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,gBAAgB,CAAA;AAC1C,OAAO,KAAK,EAAE,eAAe,EAAE,gBAAgB,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAA;AAE3F,MAAM,WAAW,gBAAgB;IAC/B,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,iEAAiE;IACjE,IAAI,CAAC,EAAE,IAAI,CAAA;IACX,iGAAiG;IACjG,KAAK,CAAC,EAAE,CAAC,GAAG,EAAE,eAAe,EAAE,GAAG,EAAE,gBAAgB,CAAC,OAAO,CAAC,KAAK,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAA;IAC5F,kEAAkE;IAClE,KAAK,CAAC,EAAE,MAAM,CAAA;CACf;AAED;;;;;;;;;GASG;AACH,eAAO,MAAM,aAAa,GAAI,OAAM,gBAAqB,KAAG,YAsB3D,CAAA;AAED;;;;GAIG;AACH,eAAO,MAAM,QAAQ,GAAI,CAAC,SAAS,WAAW,EAAE,CAAC,SAAS,YAAY,CAAC,CAAC,CAAC,EACvE,SAAS,CAAC,EACV,MAAM,IAAI,EACV,QAAO,MAAsB,KAC5B,CAGF,CAAA"}
|
package/build/guard.js
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { createService } from '@owlmeans/context';
|
|
2
|
+
import { DEFAULT_GUARD } from '@owlmeans/auth-common';
|
|
3
|
+
/**
|
|
4
|
+
* Build a `GuardService` that resolves to a chosen `Auth` without exercising
|
|
5
|
+
* any real cryptography or trusted-record lookup. Use this in category-B
|
|
6
|
+
* tests where the goal is to verify behaviour that depends on an
|
|
7
|
+
* already-authenticated identity.
|
|
8
|
+
*
|
|
9
|
+
* The mock implements the full `GuardService` shape — `match`, `handle`,
|
|
10
|
+
* `authenticated` — so it can be registered on a context exactly the way
|
|
11
|
+
* a real guard is.
|
|
12
|
+
*/
|
|
13
|
+
export const makeMockGuard = (opts = {}) => {
|
|
14
|
+
const alias = opts.alias ?? DEFAULT_GUARD;
|
|
15
|
+
const allow = opts.allow ?? (() => true);
|
|
16
|
+
const guard = createService(alias, {
|
|
17
|
+
token: opts.token,
|
|
18
|
+
match: async (req, res) => {
|
|
19
|
+
return Boolean(await allow(req, res));
|
|
20
|
+
},
|
|
21
|
+
handle: async (_req, res) => {
|
|
22
|
+
if (opts.auth != null) {
|
|
23
|
+
res.resolve(opts.auth);
|
|
24
|
+
}
|
|
25
|
+
return opts.auth;
|
|
26
|
+
},
|
|
27
|
+
authenticated: async () => opts.token ?? null,
|
|
28
|
+
});
|
|
29
|
+
return guard;
|
|
30
|
+
};
|
|
31
|
+
/**
|
|
32
|
+
* Register a mock guard on `context` that resolves to the given `auth`.
|
|
33
|
+
* Returns the same context for chaining. Existing guards under the same
|
|
34
|
+
* alias are not replaced — register before any real guard service.
|
|
35
|
+
*/
|
|
36
|
+
export const withAuth = (context, auth, alias = DEFAULT_GUARD) => {
|
|
37
|
+
const guard = makeMockGuard({ alias, auth });
|
|
38
|
+
return context.registerService(guard);
|
|
39
|
+
};
|
|
40
|
+
//# sourceMappingURL=guard.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"guard.js","sourceRoot":"","sources":["../src/guard.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAA;AAEjD,OAAO,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAA;AAcrD;;;;;;;;;GASG;AACH,MAAM,CAAC,MAAM,aAAa,GAAG,CAAC,OAAyB,EAAE,EAAgB,EAAE;IACzE,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,IAAI,aAAa,CAAA;IACzC,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,IAAI,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,CAAA;IAExC,MAAM,KAAK,GAAG,aAAa,CAAe,KAAK,EAAE;QAC/C,KAAK,EAAE,IAAI,CAAC,KAAK;QAEjB,KAAK,EAAE,KAAK,EAAE,GAAG,EAAE,GAAG,EAAE,EAAE;YACxB,OAAO,OAAO,CAAC,MAAM,KAAK,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC,CAAA;QACvC,CAAC;QAED,MAAM,EAAE,KAAK,EAAE,IAAI,EAAE,GAAG,EAAE,EAAE;YAC1B,IAAI,IAAI,CAAC,IAAI,IAAI,IAAI,EAAE,CAAC;gBACtB,GAAG,CAAC,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;YACxB,CAAC;YACD,OAAO,IAAI,CAAC,IAAa,CAAA;QAC3B,CAAC;QAED,aAAa,EAAE,KAAK,IAAI,EAAE,CAAC,IAAI,CAAC,KAAK,IAAI,IAAI;KAC9C,CAAC,CAAA;IAEF,OAAO,KAAK,CAAA;AACd,CAAC,CAAA;AAED;;;;GAIG;AACH,MAAM,CAAC,MAAM,QAAQ,GAAG,CACtB,OAAU,EACV,IAAU,EACV,QAAgB,aAAa,EAC1B,EAAE;IACL,MAAM,KAAK,GAAG,aAAa,CAAC,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAA;IAC5C,OAAO,OAAO,CAAC,eAAe,CAAI,KAAK,CAAC,CAAA;AAC1C,CAAC,CAAA"}
|
package/build/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,WAAW,CAAA;AACzB,cAAc,cAAc,CAAA;AAC5B,cAAc,YAAY,CAAA;AAC1B,cAAc,eAAe,CAAA;AAC7B,cAAc,eAAe,CAAA"}
|
package/build/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,WAAW,CAAA;AACzB,cAAc,cAAc,CAAA;AAC5B,cAAc,YAAY,CAAA;AAC1B,cAAc,eAAe,CAAA;AAC7B,cAAc,eAAe,CAAA"}
|
package/build/keys.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { KeyPairModel } from '@owlmeans/basic-keys';
|
|
2
|
+
/**
|
|
3
|
+
* Deterministic Ed25519 keypair derived from `seed`. Same seed → same keys.
|
|
4
|
+
* Use in tests to keep signatures and trust-records stable across runs.
|
|
5
|
+
*
|
|
6
|
+
* Calling without a seed returns a fresh random keypair (re-uses
|
|
7
|
+
* `makeKeyPairModel()` semantics).
|
|
8
|
+
*/
|
|
9
|
+
export declare const makeFixtureKeyPair: (seed?: string) => KeyPairModel;
|
|
10
|
+
//# sourceMappingURL=keys.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"keys.d.ts","sourceRoot":"","sources":["../src/keys.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAA;AAMxD;;;;;;GAMG;AACH,eAAO,MAAM,kBAAkB,GAAI,OAAO,MAAM,KAAG,YAMlD,CAAA"}
|
package/build/keys.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { makeKeyPairModel } from '@owlmeans/basic-keys';
|
|
2
|
+
import { createHash } from 'node:crypto';
|
|
3
|
+
const encodeBase64 = (bytes) => Buffer.from(bytes).toString('base64');
|
|
4
|
+
/**
|
|
5
|
+
* Deterministic Ed25519 keypair derived from `seed`. Same seed → same keys.
|
|
6
|
+
* Use in tests to keep signatures and trust-records stable across runs.
|
|
7
|
+
*
|
|
8
|
+
* Calling without a seed returns a fresh random keypair (re-uses
|
|
9
|
+
* `makeKeyPairModel()` semantics).
|
|
10
|
+
*/
|
|
11
|
+
export const makeFixtureKeyPair = (seed) => {
|
|
12
|
+
if (seed == null)
|
|
13
|
+
return makeKeyPairModel();
|
|
14
|
+
const primary = createHash('sha256')
|
|
15
|
+
.update(`@owlmeans/test-auth:${seed}`)
|
|
16
|
+
.digest();
|
|
17
|
+
return makeKeyPairModel(`ed25519:${encodeBase64(primary)}`);
|
|
18
|
+
};
|
|
19
|
+
//# sourceMappingURL=keys.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"keys.js","sourceRoot":"","sources":["../src/keys.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAA;AAEvD,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAA;AAExC,MAAM,YAAY,GAAG,CAAC,KAAiB,EAAU,EAAE,CACjD,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAA;AAEvC;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,kBAAkB,GAAG,CAAC,IAAa,EAAgB,EAAE;IAChE,IAAI,IAAI,IAAI,IAAI;QAAE,OAAO,gBAAgB,EAAE,CAAA;IAC3C,MAAM,OAAO,GAAG,UAAU,CAAC,QAAQ,CAAC;SACjC,MAAM,CAAC,uBAAuB,IAAI,EAAE,CAAC;SACrC,MAAM,EAAE,CAAA;IACX,OAAO,gBAAgB,CAAC,WAAW,YAAY,CAAC,OAAO,CAAC,EAAE,CAAC,CAAA;AAC7D,CAAC,CAAA"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { TrustedRecord } from '@owlmeans/auth-common';
|
|
2
|
+
import type { Resource } from '@owlmeans/resource';
|
|
3
|
+
/**
|
|
4
|
+
* In-memory `Resource<TrustedRecord>` used to satisfy `trust()` lookups
|
|
5
|
+
* (`@owlmeans/auth-common/utils/trusted.ts`). Pre-populate with the
|
|
6
|
+
* trusted users your code under test expects to find.
|
|
7
|
+
*
|
|
8
|
+
* Only `load`, `save` and `create` are implemented — that covers the
|
|
9
|
+
* `trust()` boundary plus straightforward seeding from tests. Other
|
|
10
|
+
* operations throw, so tests that need them have to be honest about
|
|
11
|
+
* adding integration coverage instead of leaning on the mock.
|
|
12
|
+
*/
|
|
13
|
+
export declare const makeMemoryTrustedResource: (records?: TrustedRecord[], alias?: string) => Resource<TrustedRecord>;
|
|
14
|
+
//# sourceMappingURL=trusted.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"trusted.d.ts","sourceRoot":"","sources":["../src/trusted.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAA;AAC1D,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAA;AAWlD;;;;;;;;;GASG;AACH,eAAO,MAAM,yBAAyB,GACpC,UAAS,aAAa,EAAO,EAC7B,QAAO,MAA8B,KACpC,QAAQ,CAAC,aAAa,CAsDxB,CAAA"}
|
package/build/trusted.js
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { appendContextual } from '@owlmeans/context';
|
|
2
|
+
const TRUSTED_DEFAULT_ALIAS = 'TRUSTED';
|
|
3
|
+
const notImplemented = (op) => {
|
|
4
|
+
throw new Error(`@owlmeans/test-auth: in-memory TRUSTED resource does not implement ${op}()`);
|
|
5
|
+
};
|
|
6
|
+
const getter = (record, field) => record[field];
|
|
7
|
+
/**
|
|
8
|
+
* In-memory `Resource<TrustedRecord>` used to satisfy `trust()` lookups
|
|
9
|
+
* (`@owlmeans/auth-common/utils/trusted.ts`). Pre-populate with the
|
|
10
|
+
* trusted users your code under test expects to find.
|
|
11
|
+
*
|
|
12
|
+
* Only `load`, `save` and `create` are implemented — that covers the
|
|
13
|
+
* `trust()` boundary plus straightforward seeding from tests. Other
|
|
14
|
+
* operations throw, so tests that need them have to be honest about
|
|
15
|
+
* adding integration coverage instead of leaning on the mock.
|
|
16
|
+
*/
|
|
17
|
+
export const makeMemoryTrustedResource = (records = [], alias = TRUSTED_DEFAULT_ALIAS) => {
|
|
18
|
+
const store = new Map();
|
|
19
|
+
for (const r of records) {
|
|
20
|
+
if (r.id == null) {
|
|
21
|
+
throw new Error('@owlmeans/test-auth: TRUSTED record without id');
|
|
22
|
+
}
|
|
23
|
+
store.set(r.id, r);
|
|
24
|
+
}
|
|
25
|
+
const findByField = (value, field) => {
|
|
26
|
+
for (const record of store.values()) {
|
|
27
|
+
if (getter(record, field) === value)
|
|
28
|
+
return record;
|
|
29
|
+
}
|
|
30
|
+
return null;
|
|
31
|
+
};
|
|
32
|
+
const resource = {
|
|
33
|
+
load: async (id, field) => {
|
|
34
|
+
const fieldName = typeof field === 'string'
|
|
35
|
+
? field
|
|
36
|
+
: field?.field;
|
|
37
|
+
if (fieldName != null && fieldName !== 'id') {
|
|
38
|
+
return findByField(id, fieldName) ?? null;
|
|
39
|
+
}
|
|
40
|
+
return store.get(id) ?? null;
|
|
41
|
+
},
|
|
42
|
+
save: async (record) => {
|
|
43
|
+
if (record.id == null) {
|
|
44
|
+
throw new Error('@owlmeans/test-auth: cannot save TRUSTED record without id');
|
|
45
|
+
}
|
|
46
|
+
store.set(record.id, record);
|
|
47
|
+
return record;
|
|
48
|
+
},
|
|
49
|
+
create: async (record) => {
|
|
50
|
+
if (record.id == null) {
|
|
51
|
+
throw new Error('@owlmeans/test-auth: cannot create TRUSTED record without id');
|
|
52
|
+
}
|
|
53
|
+
if (store.has(record.id)) {
|
|
54
|
+
throw new Error(`@owlmeans/test-auth: TRUSTED record ${record.id} already exists`);
|
|
55
|
+
}
|
|
56
|
+
store.set(record.id, record);
|
|
57
|
+
return record;
|
|
58
|
+
},
|
|
59
|
+
get: () => notImplemented('get'),
|
|
60
|
+
list: () => notImplemented('list'),
|
|
61
|
+
update: () => notImplemented('update'),
|
|
62
|
+
delete: () => notImplemented('delete'),
|
|
63
|
+
pick: () => notImplemented('pick'),
|
|
64
|
+
};
|
|
65
|
+
return appendContextual(alias, resource);
|
|
66
|
+
};
|
|
67
|
+
//# sourceMappingURL=trusted.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"trusted.js","sourceRoot":"","sources":["../src/trusted.ts"],"names":[],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@owlmeans/test-auth",
|
|
3
|
+
"version": "0.1.3",
|
|
4
|
+
"license": "MIT",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"build": "tsc -b",
|
|
8
|
+
"dev": "nodemon -e ts,tsx,json --watch src --exec \"tsc -p ./tsconfig.json\"",
|
|
9
|
+
"watch": "tsc -b -w --preserveWatchOutput --pretty"
|
|
10
|
+
},
|
|
11
|
+
"main": "build/index.js",
|
|
12
|
+
"module": "build/index.js",
|
|
13
|
+
"types": "build/index.d.ts",
|
|
14
|
+
"exports": {
|
|
15
|
+
".": {
|
|
16
|
+
"import": "./build/index.js",
|
|
17
|
+
"require": "./build/index.js",
|
|
18
|
+
"default": "./build/index.js",
|
|
19
|
+
"module": "./build/index.js",
|
|
20
|
+
"types": "./build/index.d.ts"
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
"dependencies": {
|
|
24
|
+
"@owlmeans/auth": "^0.1.3",
|
|
25
|
+
"@owlmeans/auth-common": "^0.1.3",
|
|
26
|
+
"@owlmeans/basic-envelope": "^0.1.3",
|
|
27
|
+
"@owlmeans/basic-keys": "^0.1.3",
|
|
28
|
+
"@owlmeans/context": "^0.1.3",
|
|
29
|
+
"@owlmeans/entrypoint": "^0.1.3",
|
|
30
|
+
"@owlmeans/resource": "^0.1.3",
|
|
31
|
+
"@owlmeans/test": "^0.1.2"
|
|
32
|
+
},
|
|
33
|
+
"devDependencies": {
|
|
34
|
+
"@owlmeans/dep-config": "workspace:*",
|
|
35
|
+
"@types/bun": "^1.3.0",
|
|
36
|
+
"nodemon": "^3.1.11",
|
|
37
|
+
"typescript": "^6.0.2"
|
|
38
|
+
},
|
|
39
|
+
"publishConfig": {
|
|
40
|
+
"access": "public"
|
|
41
|
+
}
|
|
42
|
+
}
|
package/src/envelope.ts
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { EnvelopeKind, makeEnvelopeModel } from '@owlmeans/basic-envelope'
|
|
2
|
+
import type { KeyPairModel } from '@owlmeans/basic-keys'
|
|
3
|
+
import { AuthroizationType } from '@owlmeans/auth'
|
|
4
|
+
import type { Auth } from '@owlmeans/auth'
|
|
5
|
+
import { makeFixtureKeyPair } from './keys.js'
|
|
6
|
+
|
|
7
|
+
const AUTH_BEARER_PREFIX = AuthroizationType.Ed25519BasicToken.toUpperCase()
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Wrap a payload in a signed `EnvelopeModel` using a fixture keypair (or one
|
|
11
|
+
* supplied by the caller) and serialize it. Useful in category-B tests that
|
|
12
|
+
* exercise envelope unwrapping/verification without hitting a real signer.
|
|
13
|
+
*/
|
|
14
|
+
export const signMockEnvelope = async <T extends {} | string>(
|
|
15
|
+
msg: T,
|
|
16
|
+
type: string,
|
|
17
|
+
kind: EnvelopeKind = EnvelopeKind.Token,
|
|
18
|
+
kp: KeyPairModel = makeFixtureKeyPair('test-auth-default')
|
|
19
|
+
): Promise<string> => {
|
|
20
|
+
const envelope = makeEnvelopeModel<T>(type)
|
|
21
|
+
envelope.send(msg)
|
|
22
|
+
const signed = await envelope.sign(kp, kind)
|
|
23
|
+
return signed
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Produce an `Authorization` header value of the form
|
|
28
|
+
* `ED25519-BASIC-TOKEN <encoded>` for a given `Auth`. Lets unit tests of
|
|
29
|
+
* header parsing and guard `match` logic generate valid-looking bearers.
|
|
30
|
+
*/
|
|
31
|
+
export const makeBearer = async (
|
|
32
|
+
auth: Auth,
|
|
33
|
+
kp?: KeyPairModel
|
|
34
|
+
): Promise<string> => {
|
|
35
|
+
const encoded = await signMockEnvelope(
|
|
36
|
+
auth as unknown as Record<string, unknown>,
|
|
37
|
+
AuthroizationType.Ed25519BasicToken,
|
|
38
|
+
EnvelopeKind.Token,
|
|
39
|
+
kp
|
|
40
|
+
)
|
|
41
|
+
return `${AUTH_BEARER_PREFIX} ${encoded}`
|
|
42
|
+
}
|
package/src/fixtures.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { AuthRole, AuthenticationType } from '@owlmeans/auth'
|
|
2
|
+
import type { Auth } from '@owlmeans/auth'
|
|
3
|
+
|
|
4
|
+
const baseAuth = (role: AuthRole, userId: string, token: string): Auth => ({
|
|
5
|
+
type: AuthenticationType.BasicEd25519,
|
|
6
|
+
role,
|
|
7
|
+
userId,
|
|
8
|
+
token,
|
|
9
|
+
isUser: role !== AuthRole.Service && role !== AuthRole.System,
|
|
10
|
+
createdAt: new Date(0),
|
|
11
|
+
scopes: ['*'],
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
export const SUPERUSER: Auth = baseAuth(AuthRole.Superuser, 'superuser-1', 'token-superuser')
|
|
15
|
+
export const USER: Auth = baseAuth(AuthRole.User, 'user-1', 'token-user')
|
|
16
|
+
export const SERVICE: Auth = baseAuth(AuthRole.Service, 'service-1', 'token-service')
|
package/src/guard.ts
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { createService } from '@owlmeans/context'
|
|
2
|
+
import type { BasicConfig, BasicContext } from '@owlmeans/context'
|
|
3
|
+
import { DEFAULT_GUARD } from '@owlmeans/auth-common'
|
|
4
|
+
import type { Auth } from '@owlmeans/auth'
|
|
5
|
+
import type { AbstractRequest, AbstractResponse, GuardService } from '@owlmeans/entrypoint'
|
|
6
|
+
|
|
7
|
+
export interface MockGuardOptions {
|
|
8
|
+
alias?: string
|
|
9
|
+
/** Auth payload that `handle` will resolve into the response. */
|
|
10
|
+
auth?: Auth
|
|
11
|
+
/** Optional predicate that decides whether `match` reports a hit. Defaults to "always match". */
|
|
12
|
+
allow?: (req: AbstractRequest, res: AbstractResponse<unknown>) => boolean | Promise<boolean>
|
|
13
|
+
/** Token returned by the client-side `authenticated()` method. */
|
|
14
|
+
token?: string
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Build a `GuardService` that resolves to a chosen `Auth` without exercising
|
|
19
|
+
* any real cryptography or trusted-record lookup. Use this in category-B
|
|
20
|
+
* tests where the goal is to verify behaviour that depends on an
|
|
21
|
+
* already-authenticated identity.
|
|
22
|
+
*
|
|
23
|
+
* The mock implements the full `GuardService` shape — `match`, `handle`,
|
|
24
|
+
* `authenticated` — so it can be registered on a context exactly the way
|
|
25
|
+
* a real guard is.
|
|
26
|
+
*/
|
|
27
|
+
export const makeMockGuard = (opts: MockGuardOptions = {}): GuardService => {
|
|
28
|
+
const alias = opts.alias ?? DEFAULT_GUARD
|
|
29
|
+
const allow = opts.allow ?? (() => true)
|
|
30
|
+
|
|
31
|
+
const guard = createService<GuardService>(alias, {
|
|
32
|
+
token: opts.token,
|
|
33
|
+
|
|
34
|
+
match: async (req, res) => {
|
|
35
|
+
return Boolean(await allow(req, res))
|
|
36
|
+
},
|
|
37
|
+
|
|
38
|
+
handle: async (_req, res) => {
|
|
39
|
+
if (opts.auth != null) {
|
|
40
|
+
res.resolve(opts.auth)
|
|
41
|
+
}
|
|
42
|
+
return opts.auth as never
|
|
43
|
+
},
|
|
44
|
+
|
|
45
|
+
authenticated: async () => opts.token ?? null,
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
return guard
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Register a mock guard on `context` that resolves to the given `auth`.
|
|
53
|
+
* Returns the same context for chaining. Existing guards under the same
|
|
54
|
+
* alias are not replaced — register before any real guard service.
|
|
55
|
+
*/
|
|
56
|
+
export const withAuth = <C extends BasicConfig, T extends BasicContext<C>>(
|
|
57
|
+
context: T,
|
|
58
|
+
auth: Auth,
|
|
59
|
+
alias: string = DEFAULT_GUARD
|
|
60
|
+
): T => {
|
|
61
|
+
const guard = makeMockGuard({ alias, auth })
|
|
62
|
+
return context.registerService<T>(guard)
|
|
63
|
+
}
|
package/src/index.ts
ADDED
package/src/keys.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { makeKeyPairModel } from '@owlmeans/basic-keys'
|
|
2
|
+
import type { KeyPairModel } from '@owlmeans/basic-keys'
|
|
3
|
+
import { createHash } from 'node:crypto'
|
|
4
|
+
|
|
5
|
+
const encodeBase64 = (bytes: Uint8Array): string =>
|
|
6
|
+
Buffer.from(bytes).toString('base64')
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Deterministic Ed25519 keypair derived from `seed`. Same seed → same keys.
|
|
10
|
+
* Use in tests to keep signatures and trust-records stable across runs.
|
|
11
|
+
*
|
|
12
|
+
* Calling without a seed returns a fresh random keypair (re-uses
|
|
13
|
+
* `makeKeyPairModel()` semantics).
|
|
14
|
+
*/
|
|
15
|
+
export const makeFixtureKeyPair = (seed?: string): KeyPairModel => {
|
|
16
|
+
if (seed == null) return makeKeyPairModel()
|
|
17
|
+
const primary = createHash('sha256')
|
|
18
|
+
.update(`@owlmeans/test-auth:${seed}`)
|
|
19
|
+
.digest()
|
|
20
|
+
return makeKeyPairModel(`ed25519:${encodeBase64(primary)}`)
|
|
21
|
+
}
|
package/src/trusted.ts
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { appendContextual } from '@owlmeans/context'
|
|
2
|
+
import type { TrustedRecord } from '@owlmeans/auth-common'
|
|
3
|
+
import type { Resource } from '@owlmeans/resource'
|
|
4
|
+
|
|
5
|
+
const TRUSTED_DEFAULT_ALIAS = 'TRUSTED'
|
|
6
|
+
|
|
7
|
+
const notImplemented = (op: string): never => {
|
|
8
|
+
throw new Error(`@owlmeans/test-auth: in-memory TRUSTED resource does not implement ${op}()`)
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const getter = (record: TrustedRecord, field: string): unknown =>
|
|
12
|
+
(record as unknown as Record<string, unknown>)[field]
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* In-memory `Resource<TrustedRecord>` used to satisfy `trust()` lookups
|
|
16
|
+
* (`@owlmeans/auth-common/utils/trusted.ts`). Pre-populate with the
|
|
17
|
+
* trusted users your code under test expects to find.
|
|
18
|
+
*
|
|
19
|
+
* Only `load`, `save` and `create` are implemented — that covers the
|
|
20
|
+
* `trust()` boundary plus straightforward seeding from tests. Other
|
|
21
|
+
* operations throw, so tests that need them have to be honest about
|
|
22
|
+
* adding integration coverage instead of leaning on the mock.
|
|
23
|
+
*/
|
|
24
|
+
export const makeMemoryTrustedResource = (
|
|
25
|
+
records: TrustedRecord[] = [],
|
|
26
|
+
alias: string = TRUSTED_DEFAULT_ALIAS
|
|
27
|
+
): Resource<TrustedRecord> => {
|
|
28
|
+
const store = new Map<string, TrustedRecord>()
|
|
29
|
+
for (const r of records) {
|
|
30
|
+
if (r.id == null) {
|
|
31
|
+
throw new Error('@owlmeans/test-auth: TRUSTED record without id')
|
|
32
|
+
}
|
|
33
|
+
store.set(r.id, r)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const findByField = (value: string, field: string): TrustedRecord | null => {
|
|
37
|
+
for (const record of store.values()) {
|
|
38
|
+
if (getter(record, field) === value) return record
|
|
39
|
+
}
|
|
40
|
+
return null
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const resource: Partial<Resource<TrustedRecord>> = {
|
|
44
|
+
load: async <Type extends TrustedRecord>(id: string, field?: unknown) => {
|
|
45
|
+
const fieldName = typeof field === 'string'
|
|
46
|
+
? field
|
|
47
|
+
: (field as { field?: string } | undefined)?.field
|
|
48
|
+
if (fieldName != null && fieldName !== 'id') {
|
|
49
|
+
return (findByField(id, fieldName) as Type | null) ?? null
|
|
50
|
+
}
|
|
51
|
+
return (store.get(id) as Type | undefined) ?? null
|
|
52
|
+
},
|
|
53
|
+
|
|
54
|
+
save: async <Type extends TrustedRecord>(record: Partial<Type>) => {
|
|
55
|
+
if (record.id == null) {
|
|
56
|
+
throw new Error('@owlmeans/test-auth: cannot save TRUSTED record without id')
|
|
57
|
+
}
|
|
58
|
+
store.set(record.id, record as TrustedRecord)
|
|
59
|
+
return record as Type
|
|
60
|
+
},
|
|
61
|
+
|
|
62
|
+
create: async <Type extends TrustedRecord>(record: Partial<Type>) => {
|
|
63
|
+
if (record.id == null) {
|
|
64
|
+
throw new Error('@owlmeans/test-auth: cannot create TRUSTED record without id')
|
|
65
|
+
}
|
|
66
|
+
if (store.has(record.id)) {
|
|
67
|
+
throw new Error(`@owlmeans/test-auth: TRUSTED record ${record.id} already exists`)
|
|
68
|
+
}
|
|
69
|
+
store.set(record.id, record as TrustedRecord)
|
|
70
|
+
return record as Type
|
|
71
|
+
},
|
|
72
|
+
|
|
73
|
+
get: () => notImplemented('get'),
|
|
74
|
+
list: () => notImplemented('list'),
|
|
75
|
+
update: () => notImplemented('update'),
|
|
76
|
+
delete: () => notImplemented('delete'),
|
|
77
|
+
pick: () => notImplemented('pick'),
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return appendContextual<Resource<TrustedRecord>>(alias, resource)
|
|
81
|
+
}
|
package/tsconfig.json
ADDED