@owlmeans/auth-common 0.1.18-rc.29 → 0.1.18-rc.30

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 CHANGED
@@ -12,7 +12,7 @@ Predefined authentication protocols, guards, and constants shared between server
12
12
  ## Installation
13
13
 
14
14
  ```bash
15
- bun add @owlmeans/auth-common@^0.1.18-rc.29
15
+ bun add @owlmeans/auth-common@^0.1.18-rc.30
16
16
  ```
17
17
 
18
18
  ## Usage
@@ -98,7 +98,7 @@ This package ships embedded agent skills under `agent-meta/`. After installing y
98
98
  your project's skill store (`.agents/skills/`):
99
99
 
100
100
  ```sh
101
- npx @owlmeans/agent-skills@^0.1.18-rc.28
101
+ npx @owlmeans/agent-skills@^0.1.18-rc.30
102
102
  ```
103
103
 
104
104
  The embedded files are version-matched to this package release. Do not edit them
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "schemaVersion": 2,
3
3
  "package": "@owlmeans/auth-common",
4
- "version": "0.1.18-rc.29",
5
- "generatedAt": "2026-09-17T08:16:11.214Z",
4
+ "version": "0.1.18-rc.30",
5
+ "generatedAt": "2026-09-19T13:42:56.242Z",
6
6
  "canonicalRepo": "https://github.com/owlmeans/common",
7
7
  "entries": [
8
8
  {
@@ -8,7 +8,7 @@ user-invocable: false
8
8
  # @owlmeans/auth-common
9
9
 
10
10
  **Layer:** Core
11
- **Install:** `"@owlmeans/auth-common": "^0.1.18-rc.29"` in `dependencies`
11
+ **Install:** `"@owlmeans/auth-common": "^0.1.18-rc.30"` in `dependencies`
12
12
 
13
13
  Everything a server and a browser must agree on to talk authentication: aliases, the shared
14
14
  protocol declarations, the signature guard, and the contract for resolving the organization
@@ -128,7 +128,10 @@ depends on the value.
128
128
  the matching client-side alias as `DEFAULT_ALIAS`.
129
129
  - `GUARD_ED25519` is for service-to-service calls, where the caller signs the request rather than
130
130
  presenting a bearer token. Give it a Redis-backed nonce cache (`BED255_CASHE_RESOURCE`) in any
131
- deployment that runs more than one replica, or replay protection is per-process only.
131
+ deployment that runs more than one replica, or replay protection is per-process only. The guard
132
+ accepts only canonical timestamps inside its symmetric 60-second window and claims the
133
+ credential-and-nonce pair until that timestamp's absolute expiry; a replay-store error refuses
134
+ authentication rather than disabling deduplication.
132
135
  - An OIDC gate is not required when OIDC is only the login/bootstrap provider. Where the provider
133
136
  issues a local bearer token, authorize with a product-specific gate against local identity scopes.
134
137
  - The trust resource (`TRUSTED`, from `@owlmeans/config`) is the system's source of truth for known
@@ -1,4 +1,5 @@
1
1
  export type * from './types.js';
2
2
  export * from './consts.js';
3
+ export * from './replay.js';
3
4
  export * from './service.js';
4
5
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/guards/basic-ed25519/index.ts"],"names":[],"mappings":"AACA,mBAAmB,YAAY,CAAA;AAC/B,cAAc,aAAa,CAAA;AAC3B,cAAc,cAAc,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/guards/basic-ed25519/index.ts"],"names":[],"mappings":"AACA,mBAAmB,YAAY,CAAA;AAC/B,cAAc,aAAa,CAAA;AAC3B,cAAc,aAAa,CAAA;AAC3B,cAAc,cAAc,CAAA"}
@@ -1,3 +1,4 @@
1
1
  export * from './consts.js';
2
+ export * from './replay.js';
2
3
  export * from './service.js';
3
4
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/guards/basic-ed25519/index.ts"],"names":[],"mappings":"AAEA,cAAc,aAAa,CAAA;AAC3B,cAAc,cAAc,CAAA"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/guards/basic-ed25519/index.ts"],"names":[],"mappings":"AAEA,cAAc,aAAa,CAAA;AAC3B,cAAc,aAAa,CAAA;AAC3B,cAAc,cAAc,CAAA"}
@@ -0,0 +1,14 @@
1
+ import type { SignedRequestReplayResource, SignedRequestReplayStore } from './types.js';
2
+ /**
3
+ * Process-local replay protection used when a deployment has not supplied a shared resource.
4
+ * A timer removes each claim at its signed expiry, keeping the fallback bounded by the live
5
+ * replay window rather than by process lifetime.
6
+ */
7
+ export declare const makeMemorySignedRequestReplayStore: () => SignedRequestReplayStore;
8
+ /**
9
+ * Adapt a create-once Resource to signed-request replay protection. Redis resources honour the
10
+ * absolute expiry and make the claim atomic across replicas (`SET NX`); other resource failures
11
+ * remain failures rather than being mistaken for successful claims.
12
+ */
13
+ export declare const makeResourceSignedRequestReplayStore: (resource: SignedRequestReplayResource) => SignedRequestReplayStore;
14
+ //# sourceMappingURL=replay.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"replay.d.ts","sourceRoot":"","sources":["../../../src/guards/basic-ed25519/replay.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,2BAA2B,EAAE,wBAAwB,EAAE,MAAM,YAAY,CAAA;AAEvF;;;;GAIG;AACH,eAAO,MAAM,kCAAkC,QAAO,wBAwBrD,CAAA;AAED;;;;GAIG;AACH,eAAO,MAAM,oCAAoC,aACrC,2BAA2B,KACpC,wBAaD,CAAA"}
@@ -0,0 +1,49 @@
1
+ import { RecordExists } from '@owlmeans/resource';
2
+ /**
3
+ * Process-local replay protection used when a deployment has not supplied a shared resource.
4
+ * A timer removes each claim at its signed expiry, keeping the fallback bounded by the live
5
+ * replay window rather than by process lifetime.
6
+ */
7
+ export const makeMemorySignedRequestReplayStore = () => {
8
+ const claims = new Map();
9
+ return {
10
+ claim: async (id, expiresAt) => {
11
+ const now = Date.now();
12
+ const current = claims.get(id);
13
+ if (current != null && current > now) {
14
+ return false;
15
+ }
16
+ const expiry = expiresAt.getTime();
17
+ claims.set(id, expiry);
18
+ const timer = setTimeout(() => {
19
+ if (claims.get(id) === expiry) {
20
+ claims.delete(id);
21
+ }
22
+ }, Math.max(0, expiry - now));
23
+ const unref = timer.unref;
24
+ unref?.call(timer);
25
+ return true;
26
+ }
27
+ };
28
+ };
29
+ /**
30
+ * Adapt a create-once Resource to signed-request replay protection. Redis resources honour the
31
+ * absolute expiry and make the claim atomic across replicas (`SET NX`); other resource failures
32
+ * remain failures rather than being mistaken for successful claims.
33
+ */
34
+ export const makeResourceSignedRequestReplayStore = (resource) => ({
35
+ claim: async (id, expiresAt) => {
36
+ try {
37
+ await resource.create({ id }, { ttl: expiresAt });
38
+ return true;
39
+ }
40
+ catch (error) {
41
+ if (error instanceof RecordExists
42
+ || error?.type === RecordExists.typeName) {
43
+ return false;
44
+ }
45
+ throw error;
46
+ }
47
+ }
48
+ });
49
+ //# sourceMappingURL=replay.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"replay.js","sourceRoot":"","sources":["../../../src/guards/basic-ed25519/replay.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAA;AAGjD;;;;GAIG;AACH,MAAM,CAAC,MAAM,kCAAkC,GAAG,GAA6B,EAAE;IAC/E,MAAM,MAAM,GAAG,IAAI,GAAG,EAAkB,CAAA;IAExC,OAAO;QACL,KAAK,EAAE,KAAK,EAAE,EAAE,EAAE,SAAS,EAAE,EAAE;YAC7B,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE,CAAA;YACtB,MAAM,OAAO,GAAG,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC,CAAA;YAC9B,IAAI,OAAO,IAAI,IAAI,IAAI,OAAO,GAAG,GAAG,EAAE,CAAC;gBACrC,OAAO,KAAK,CAAA;YACd,CAAC;YAED,MAAM,MAAM,GAAG,SAAS,CAAC,OAAO,EAAE,CAAA;YAClC,MAAM,CAAC,GAAG,CAAC,EAAE,EAAE,MAAM,CAAC,CAAA;YACtB,MAAM,KAAK,GAAG,UAAU,CAAC,GAAG,EAAE;gBAC5B,IAAI,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC,KAAK,MAAM,EAAE,CAAC;oBAC9B,MAAM,CAAC,MAAM,CAAC,EAAE,CAAC,CAAA;gBACnB,CAAC;YACH,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,MAAM,GAAG,GAAG,CAAC,CAAC,CAAA;YAC7B,MAAM,KAAK,GAAI,KAA2C,CAAC,KAAK,CAAA;YAChE,KAAK,EAAE,IAAI,CAAC,KAAK,CAAC,CAAA;YAElB,OAAO,IAAI,CAAA;QACb,CAAC;KACF,CAAA;AACH,CAAC,CAAA;AAED;;;;GAIG;AACH,MAAM,CAAC,MAAM,oCAAoC,GAAG,CAClD,QAAqC,EACX,EAAE,CAAC,CAAC;IAC9B,KAAK,EAAE,KAAK,EAAE,EAAE,EAAE,SAAS,EAAE,EAAE;QAC7B,IAAI,CAAC;YACH,MAAM,QAAQ,CAAC,MAAM,CAAC,EAAE,EAAE,EAAE,EAAE,EAAE,GAAG,EAAE,SAAS,EAAE,CAAC,CAAA;YACjD,OAAO,IAAI,CAAA;QACb,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,IAAI,KAAK,YAAY,YAAY;mBAC3B,KAAmC,EAAE,IAAI,KAAK,YAAY,CAAC,QAAQ,EAAE,CAAC;gBAC1E,OAAO,KAAK,CAAA;YACd,CAAC;YACD,MAAM,KAAK,CAAA;QACb,CAAC;IACH,CAAC;CACF,CAAC,CAAA"}
@@ -1,8 +1,3 @@
1
1
  import type { BasicEd25519Guard, BasicEd25519GuardOptions } from './types.js';
2
- /**
3
- * Proper configuration:
4
- * - One needs to specify opts.cache as a redis resource that stores nonces for a while to
5
- * avoid requests duplication.
6
- */
7
2
  export declare const makeBasicEd25519Guard: (resource: string, opts?: BasicEd25519GuardOptions) => BasicEd25519Guard;
8
3
  //# sourceMappingURL=service.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"service.d.ts","sourceRoot":"","sources":["../../../src/guards/basic-ed25519/service.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,iBAAiB,EAAE,wBAAwB,EAAE,MAAM,YAAY,CAAA;AAU7E;;;;GAIG;AACH,eAAO,MAAM,qBAAqB,aAAc,MAAM,SAAS,wBAAwB,KAAG,iBAqHzF,CAAA"}
1
+ {"version":3,"file":"service.d.ts","sourceRoot":"","sources":["../../../src/guards/basic-ed25519/service.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,iBAAiB,EAAE,wBAAwB,EAAE,MAAM,YAAY,CAAA;AAa7E,eAAO,MAAM,qBAAqB,aAAc,MAAM,SAAS,wBAAwB,KAAG,iBA+IzF,CAAA"}
@@ -5,17 +5,21 @@ import { trust } from '../../utils/trusted.js';
5
5
  import { AuthenPayloadError, AuthroizationType, AuthRole } from '@owlmeans/auth';
6
6
  import { createIdOfLength } from '@owlmeans/basic-ids';
7
7
  import { extractAuthToken } from '@owlmeans/auth-common/utils';
8
+ import { makeMemorySignedRequestReplayStore, makeResourceSignedRequestReplayStore } from './replay.js';
8
9
  const timeKey = BED255_TIME_HEADER.toLocaleLowerCase();
9
10
  const nonceKey = BED255_NONCE_HEADER.toLocaleLowerCase();
10
- /**
11
- * Proper configuration:
12
- * - One needs to specify opts.cache as a redis resource that stores nonces for a while to
13
- * avoid requests duplication.
14
- */
15
11
  export const makeBasicEd25519Guard = (resource, opts) => {
16
- const cache = (context) => context.hasResource(opts?.cache ?? BED255_CASHE_RESOURCE)
17
- ? context.resource(opts?.cache ?? BED255_CASHE_RESOURCE)
18
- : null;
12
+ const memoryReplay = makeMemorySignedRequestReplayStore();
13
+ const replay = (context) => {
14
+ if (opts?.replay != null) {
15
+ return opts.replay;
16
+ }
17
+ const alias = opts?.cache ?? BED255_CASHE_RESOURCE;
18
+ if (context.hasResource(alias)) {
19
+ return makeResourceSignedRequestReplayStore(context.resource(alias));
20
+ }
21
+ return memoryReplay;
22
+ };
19
23
  const guard = createService(GUARD_ED25519, {
20
24
  authenticated: async (req) => {
21
25
  const context = assertContext(guard.ctx);
@@ -54,32 +58,43 @@ export const makeBasicEd25519Guard = (resource, opts) => {
54
58
  const [key, value] = pair.split('=', 2);
55
59
  return { ...result, [key.toLowerCase()]: value };
56
60
  }, {});
61
+ if (typeof signature.credential !== 'string' || signature.credential.length === 0
62
+ || typeof signature.signature !== 'string' || signature.signature.length === 0) {
63
+ throw new AuthenPayloadError('signature');
64
+ }
57
65
  const trusted = await trust(context, resource, signature.credential, "id");
58
66
  if (trusted.user.credential == null) {
59
67
  return false;
60
68
  }
69
+ const timestamp = req.headers?.[timeKey];
70
+ const nonce = req.headers?.[nonceKey];
61
71
  const payload = {
62
72
  body: req.body ?? {},
63
- headers: { [timeKey]: req.headers[timeKey], [nonceKey]: req.headers[nonceKey] }
73
+ headers: { [timeKey]: timestamp, [nonceKey]: nonce }
64
74
  };
65
75
  if (!await trusted.key.verify(payload, signature.signature)) {
66
76
  return false;
67
77
  }
68
- if (typeof req.headers[timeKey] !== 'string') {
78
+ if (typeof timestamp !== 'string') {
79
+ throw new AuthenPayloadError('timestamp');
80
+ }
81
+ const createdAt = new Date(timestamp);
82
+ const signedAt = createdAt.getTime();
83
+ if (!Number.isFinite(signedAt) || createdAt.toISOString() !== timestamp) {
69
84
  throw new AuthenPayloadError('timestamp');
70
85
  }
71
- const createdAt = new Date(req.headers[timeKey]);
72
- if (createdAt.getTime() + BED255_SIG_TTL < Date.now()) {
86
+ if (Math.abs(Date.now() - signedAt) > BED255_SIG_TTL) {
73
87
  throw new AuthenPayloadError('expired');
74
88
  }
75
- const nonce = req.headers[nonceKey];
76
- if (typeof nonce !== 'string') {
89
+ if (typeof nonce !== 'string' || nonce.length === 0) {
77
90
  throw new AuthenPayloadError('nonce');
78
91
  }
79
92
  try {
80
- const tried = cache(context);
81
- if (tried != null) {
82
- await tried.create({ id: nonce }, { ttl: BED255_SIG_TTL / 1000 });
93
+ // The replay identity is the signing credential plus its nonce. A nonce used by one
94
+ // trusted service does not block another, while a captured request can be admitted once.
95
+ const claimed = await replay(context).claim(`${signature.credential.length}:${signature.credential}${nonce}`, new Date(signedAt + BED255_SIG_TTL));
96
+ if (!claimed) {
97
+ throw new AuthenPayloadError('nonce');
83
98
  }
84
99
  }
85
100
  catch {
@@ -1 +1 @@
1
- {"version":3,"file":"service.js","sourceRoot":"","sources":["../../../src/guards/basic-ed25519/service.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"service.js","sourceRoot":"","sources":["../../../src/guards/basic-ed25519/service.ts"],"names":[],"mappings":"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"}
@@ -1,7 +1,23 @@
1
1
  import type { GuardService } from '@owlmeans/entrypoint';
2
+ import type { ResourceRecord, WriteOptions } from '@owlmeans/resource';
3
+ import type { BasicResource } from '@owlmeans/context';
2
4
  export interface BasicEd25519Guard extends GuardService {
3
5
  }
4
6
  export interface BasicEd25519GuardOptions {
7
+ /**
8
+ * Legacy resource alias. A Redis resource registered under this alias gives replay protection
9
+ * shared by every replica; when it is absent the guard falls back to its process-local store.
10
+ */
5
11
  cache?: string;
12
+ /** Explicit replay store, for consumers that do not register a Resource on the context. */
13
+ replay?: SignedRequestReplayStore;
14
+ }
15
+ /** A create-once claim over one signed request identity. */
16
+ export interface SignedRequestReplayStore {
17
+ claim: (id: string, expiresAt: Date) => Promise<boolean>;
18
+ }
19
+ /** The small Resource surface needed by the Redis-compatible replay adapter. */
20
+ export interface SignedRequestReplayResource extends BasicResource {
21
+ create: (record: Partial<ResourceRecord>, opts?: WriteOptions) => Promise<ResourceRecord>;
6
22
  }
7
23
  //# sourceMappingURL=types.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/guards/basic-ed25519/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAA;AAExD,MAAM,WAAW,iBAAkB,SAAQ,YAAY;CACtD;AAED,MAAM,WAAW,wBAAwB;IACvC,KAAK,CAAC,EAAE,MAAM,CAAA;CACf"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/guards/basic-ed25519/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAA;AACxD,OAAO,KAAK,EAAE,cAAc,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAA;AACtE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAA;AAEtD,MAAM,WAAW,iBAAkB,SAAQ,YAAY;CACtD;AAED,MAAM,WAAW,wBAAwB;IACvC;;;OAGG;IACH,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,2FAA2F;IAC3F,MAAM,CAAC,EAAE,wBAAwB,CAAA;CAClC;AAED,4DAA4D;AAC5D,MAAM,WAAW,wBAAwB;IACvC,KAAK,EAAE,CAAC,EAAE,EAAE,MAAM,EAAE,SAAS,EAAE,IAAI,KAAK,OAAO,CAAC,OAAO,CAAC,CAAA;CACzD;AAED,gFAAgF;AAChF,MAAM,WAAW,2BAA4B,SAAQ,aAAa;IAChE,MAAM,EAAE,CAAC,MAAM,EAAE,OAAO,CAAC,cAAc,CAAC,EAAE,IAAI,CAAC,EAAE,YAAY,KAAK,OAAO,CAAC,cAAc,CAAC,CAAA;CAC1F"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@owlmeans/auth-common",
3
- "version": "0.1.18-rc.29",
3
+ "version": "0.1.18-rc.30",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "scripts": {
@@ -29,19 +29,19 @@
29
29
  }
30
30
  },
31
31
  "dependencies": {
32
- "@owlmeans/auth": "^0.1.18-rc.27",
33
- "@owlmeans/auth-common": "^0.1.18-rc.29",
34
- "@owlmeans/basic-ids": "^0.1.18-rc.26",
35
- "@owlmeans/basic-keys": "^0.1.18-rc.30",
36
- "@owlmeans/client-entrypoint": "^0.1.18-rc.29",
37
- "@owlmeans/context": "^0.1.18-rc.25",
38
- "@owlmeans/entrypoint": "^0.1.18-rc.28",
39
- "@owlmeans/resource": "^0.1.18-rc.27",
40
- "@owlmeans/route": "^0.1.18-rc.26"
32
+ "@owlmeans/auth": "^0.1.18-rc.28",
33
+ "@owlmeans/auth-common": "^0.1.18-rc.30",
34
+ "@owlmeans/basic-ids": "^0.1.18-rc.27",
35
+ "@owlmeans/basic-keys": "^0.1.18-rc.31",
36
+ "@owlmeans/client-entrypoint": "^0.1.18-rc.30",
37
+ "@owlmeans/context": "^0.1.18-rc.26",
38
+ "@owlmeans/entrypoint": "^0.1.18-rc.29",
39
+ "@owlmeans/resource": "^0.1.18-rc.28",
40
+ "@owlmeans/route": "^0.1.18-rc.27"
41
41
  },
42
42
  "devDependencies": {
43
43
  "@owlmeans/dep-config": "workspace:*",
44
- "@owlmeans/test-auth": "^0.1.18-rc.28",
44
+ "@owlmeans/test-auth": "^0.1.18-rc.29",
45
45
  "@types/bun": "^1.4.0",
46
46
  "nodemon": "^3.1.14",
47
47
  "typescript": "^7.0.2"
@@ -1,4 +1,5 @@
1
1
 
2
2
  export type * from './types.js'
3
3
  export * from './consts.js'
4
+ export * from './replay.js'
4
5
  export * from './service.js'
@@ -0,0 +1,55 @@
1
+ import { RecordExists } from '@owlmeans/resource'
2
+ import type { SignedRequestReplayResource, SignedRequestReplayStore } from './types.js'
3
+
4
+ /**
5
+ * Process-local replay protection used when a deployment has not supplied a shared resource.
6
+ * A timer removes each claim at its signed expiry, keeping the fallback bounded by the live
7
+ * replay window rather than by process lifetime.
8
+ */
9
+ export const makeMemorySignedRequestReplayStore = (): SignedRequestReplayStore => {
10
+ const claims = new Map<string, number>()
11
+
12
+ return {
13
+ claim: async (id, expiresAt) => {
14
+ const now = Date.now()
15
+ const current = claims.get(id)
16
+ if (current != null && current > now) {
17
+ return false
18
+ }
19
+
20
+ const expiry = expiresAt.getTime()
21
+ claims.set(id, expiry)
22
+ const timer = setTimeout(() => {
23
+ if (claims.get(id) === expiry) {
24
+ claims.delete(id)
25
+ }
26
+ }, Math.max(0, expiry - now))
27
+ const unref = (timer as unknown as { unref?: () => void }).unref
28
+ unref?.call(timer)
29
+
30
+ return true
31
+ }
32
+ }
33
+ }
34
+
35
+ /**
36
+ * Adapt a create-once Resource to signed-request replay protection. Redis resources honour the
37
+ * absolute expiry and make the claim atomic across replicas (`SET NX`); other resource failures
38
+ * remain failures rather than being mistaken for successful claims.
39
+ */
40
+ export const makeResourceSignedRequestReplayStore = (
41
+ resource: SignedRequestReplayResource
42
+ ): SignedRequestReplayStore => ({
43
+ claim: async (id, expiresAt) => {
44
+ try {
45
+ await resource.create({ id }, { ttl: expiresAt })
46
+ return true
47
+ } catch (error) {
48
+ if (error instanceof RecordExists
49
+ || (error as { type?: unknown } | null)?.type === RecordExists.typeName) {
50
+ return false
51
+ }
52
+ throw error
53
+ }
54
+ }
55
+ })
@@ -11,22 +11,30 @@ import type { BasicEd25519Guard, BasicEd25519GuardOptions } from './types.js'
11
11
  import { AuthenPayloadError, AuthroizationType, AuthRole } from '@owlmeans/auth'
12
12
  import type { Auth } from '@owlmeans/auth'
13
13
  import { createIdOfLength } from '@owlmeans/basic-ids'
14
- import type { Resource, ResourceRecord } from '@owlmeans/resource'
15
14
  import { extractAuthToken } from '@owlmeans/auth-common/utils'
15
+ import {
16
+ makeMemorySignedRequestReplayStore, makeResourceSignedRequestReplayStore
17
+ } from './replay.js'
18
+ import type { SignedRequestReplayResource, SignedRequestReplayStore } from './types.js'
16
19
 
17
20
  const timeKey = BED255_TIME_HEADER.toLocaleLowerCase()
18
21
  const nonceKey = BED255_NONCE_HEADER.toLocaleLowerCase()
19
22
 
20
- /**
21
- * Proper configuration:
22
- * - One needs to specify opts.cache as a redis resource that stores nonces for a while to
23
- * avoid requests duplication.
24
- */
25
23
  export const makeBasicEd25519Guard = (resource: string, opts?: BasicEd25519GuardOptions): BasicEd25519Guard => {
24
+ const memoryReplay = makeMemorySignedRequestReplayStore()
26
25
 
27
- const cache = (context: Context) => context.hasResource(opts?.cache ?? BED255_CASHE_RESOURCE)
28
- ? context.resource<Resource<ResourceRecord>>(opts?.cache ?? BED255_CASHE_RESOURCE)
29
- : null
26
+ const replay = (context: Context): SignedRequestReplayStore => {
27
+ if (opts?.replay != null) {
28
+ return opts.replay
29
+ }
30
+ const alias = opts?.cache ?? BED255_CASHE_RESOURCE
31
+ if (context.hasResource(alias)) {
32
+ return makeResourceSignedRequestReplayStore(
33
+ context.resource<SignedRequestReplayResource>(alias)
34
+ )
35
+ }
36
+ return memoryReplay
37
+ }
30
38
 
31
39
  const guard: BasicEd25519Guard = createService<BasicEd25519Guard>(GUARD_ED25519, {
32
40
  authenticated: async req => {
@@ -78,39 +86,55 @@ export const makeBasicEd25519Guard = (resource: string, opts?: BasicEd25519Guard
78
86
  return { ...result, [key.toLowerCase()]: value }
79
87
  }, {} as { credential: string, signature: string })
80
88
 
89
+ if (typeof signature.credential !== 'string' || signature.credential.length === 0
90
+ || typeof signature.signature !== 'string' || signature.signature.length === 0) {
91
+ throw new AuthenPayloadError('signature')
92
+ }
93
+
81
94
  const trusted = await trust(context, resource, signature.credential, "id")
82
95
  if (trusted.user.credential == null) {
83
96
  return false as T
84
97
  }
85
98
 
99
+ const timestamp = req.headers?.[timeKey]
100
+ const nonce = req.headers?.[nonceKey]
101
+
86
102
  const payload = {
87
103
  body: req.body ?? {},
88
- headers: { [timeKey]: req.headers[timeKey], [nonceKey]: req.headers[nonceKey] }
104
+ headers: { [timeKey]: timestamp, [nonceKey]: nonce }
89
105
  }
90
106
 
91
107
  if (!await trusted.key.verify(payload, signature.signature)) {
92
108
  return false as T
93
109
  }
94
110
 
95
- if (typeof req.headers[timeKey] !== 'string') {
111
+ if (typeof timestamp !== 'string') {
96
112
  throw new AuthenPayloadError('timestamp')
97
113
  }
98
114
 
99
- const createdAt = new Date(req.headers[timeKey])
115
+ const createdAt = new Date(timestamp)
116
+ const signedAt = createdAt.getTime()
117
+ if (!Number.isFinite(signedAt) || createdAt.toISOString() !== timestamp) {
118
+ throw new AuthenPayloadError('timestamp')
119
+ }
100
120
 
101
- if (createdAt.getTime() + BED255_SIG_TTL < Date.now()) {
121
+ if (Math.abs(Date.now() - signedAt) > BED255_SIG_TTL) {
102
122
  throw new AuthenPayloadError('expired')
103
123
  }
104
124
 
105
- const nonce = req.headers[nonceKey]
106
- if (typeof nonce !== 'string') {
125
+ if (typeof nonce !== 'string' || nonce.length === 0) {
107
126
  throw new AuthenPayloadError('nonce')
108
127
  }
109
128
 
110
129
  try {
111
- const tried = cache(context)
112
- if (tried != null) {
113
- await tried.create({ id: nonce }, { ttl: BED255_SIG_TTL / 1000 })
130
+ // The replay identity is the signing credential plus its nonce. A nonce used by one
131
+ // trusted service does not block another, while a captured request can be admitted once.
132
+ const claimed = await replay(context).claim(
133
+ `${signature.credential.length}:${signature.credential}${nonce}`,
134
+ new Date(signedAt + BED255_SIG_TTL)
135
+ )
136
+ if (!claimed) {
137
+ throw new AuthenPayloadError('nonce')
114
138
  }
115
139
  } catch {
116
140
  throw new AuthenPayloadError('nonce')
@@ -1,9 +1,26 @@
1
1
  import type { GuardService } from '@owlmeans/entrypoint'
2
+ import type { ResourceRecord, WriteOptions } from '@owlmeans/resource'
3
+ import type { BasicResource } from '@owlmeans/context'
2
4
 
3
5
  export interface BasicEd25519Guard extends GuardService {
4
6
  }
5
7
 
6
8
  export interface BasicEd25519GuardOptions {
9
+ /**
10
+ * Legacy resource alias. A Redis resource registered under this alias gives replay protection
11
+ * shared by every replica; when it is absent the guard falls back to its process-local store.
12
+ */
7
13
  cache?: string
14
+ /** Explicit replay store, for consumers that do not register a Resource on the context. */
15
+ replay?: SignedRequestReplayStore
8
16
  }
9
17
 
18
+ /** A create-once claim over one signed request identity. */
19
+ export interface SignedRequestReplayStore {
20
+ claim: (id: string, expiresAt: Date) => Promise<boolean>
21
+ }
22
+
23
+ /** The small Resource surface needed by the Redis-compatible replay adapter. */
24
+ export interface SignedRequestReplayResource extends BasicResource {
25
+ create: (record: Partial<ResourceRecord>, opts?: WriteOptions) => Promise<ResourceRecord>
26
+ }
@@ -0,0 +1,99 @@
1
+ import { describe, expect, test } from 'bun:test'
2
+ import { AuthenPayloadError, AuthroizationType } from '@owlmeans/auth'
3
+ import {
4
+ BED255_NONCE_HEADER, BED255_SIG_TTL, BED255_TIME_HEADER, makeBasicEd25519Guard,
5
+ makeResourceSignedRequestReplayStore
6
+ } from '@owlmeans/auth-common'
7
+ import type { SignedRequestReplayResource } from '@owlmeans/auth-common'
8
+ import type { AbstractRequest, AbstractResponse } from '@owlmeans/entrypoint'
9
+ import type { Auth } from '@owlmeans/auth'
10
+ import { RecordExists } from '@owlmeans/resource'
11
+ import { makeTrustFixture, TRUSTED_ALIAS } from './context.js'
12
+
13
+ const timeKey = BED255_TIME_HEADER.toLowerCase()
14
+ const nonceKey = BED255_NONCE_HEADER.toLowerCase()
15
+
16
+ const request = async (timestamp: string, nonce: string) => {
17
+ const fixture = makeTrustFixture({ withSecret: true, name: 'auth-common-tests' })
18
+ const body = { action: 'publish' }
19
+ const signature = await fixture.authKey.sign({
20
+ body,
21
+ headers: { [timeKey]: timestamp, [nonceKey]: nonce }
22
+ })
23
+ const req = {
24
+ alias: 'signed', body, headers: {
25
+ [timeKey]: timestamp,
26
+ [nonceKey]: nonce,
27
+ authorization: `${AuthroizationType.Ed25519BasicSignature.toUpperCase()} `
28
+ + `Credential=${fixture.authRecord.id} Signature=${signature}`,
29
+ }, params: {}, query: {}, path: '/signed'
30
+ } satisfies AbstractRequest
31
+ const guard = makeBasicEd25519Guard(TRUSTED_ALIAS)
32
+ fixture.ctx.registerService(guard)
33
+
34
+ return { fixture, guard, req }
35
+ }
36
+
37
+ const handle = async (
38
+ guard: ReturnType<typeof makeBasicEd25519Guard>, req: AbstractRequest
39
+ ): Promise<Auth | undefined> => {
40
+ let value: Auth | undefined
41
+ const response: AbstractResponse<Auth> = {
42
+ resolve: resolved => { value = resolved },
43
+ reject: error => { throw error }
44
+ }
45
+ await guard.handle(req, response)
46
+ return value
47
+ }
48
+
49
+ describe('@owlmeans/auth-common — signed request freshness and replay protection', () => {
50
+ test('accepts a canonical current timestamp once and rejects the replay by default', async () => {
51
+ const signed = await request(new Date().toISOString(), 'one-use-nonce')
52
+
53
+ expect((await handle(signed.guard, signed.req))?.userId).toBe(signed.fixture.authRecord.id)
54
+ await expect(handle(signed.guard, signed.req)).rejects.toBeInstanceOf(AuthenPayloadError)
55
+ })
56
+
57
+ test('rejects parseable but non-canonical timestamps', async () => {
58
+ const signed = await request(new Date().toUTCString(), 'non-canonical')
59
+
60
+ await expect(handle(signed.guard, signed.req)).rejects.toBeInstanceOf(AuthenPayloadError)
61
+ })
62
+
63
+ test('rejects timestamps outside the symmetric 60-second window', async () => {
64
+ const past = await request(new Date(Date.now() - BED255_SIG_TTL - 1_000).toISOString(), 'past')
65
+ const future = await request(new Date(Date.now() + BED255_SIG_TTL + 1_000).toISOString(), 'future')
66
+
67
+ await expect(handle(past.guard, past.req)).rejects.toBeInstanceOf(AuthenPayloadError)
68
+ await expect(handle(future.guard, future.req)).rejects.toBeInstanceOf(AuthenPayloadError)
69
+ })
70
+
71
+ test('rejects authentication when an injected replay store fails', async () => {
72
+ const signed = await request(new Date().toISOString(), 'store-failure')
73
+ const guard = makeBasicEd25519Guard(TRUSTED_ALIAS, {
74
+ replay: { claim: async () => { throw new Error('store unavailable') } }
75
+ })
76
+ signed.fixture.ctx.registerService(guard)
77
+
78
+ await expect(handle(guard, signed.req)).rejects.toBeInstanceOf(AuthenPayloadError)
79
+ })
80
+
81
+ test('adapts a create-once resource and forwards the absolute signed expiry', async () => {
82
+ let claimed = false
83
+ let observedExpiry: Date | undefined
84
+ const resource = {
85
+ create: async (record: { id?: string }, opts?: { ttl?: number | Date }) => {
86
+ if (claimed) throw new RecordExists(record.id ?? '')
87
+ claimed = true
88
+ observedExpiry = opts?.ttl as Date
89
+ return record
90
+ }
91
+ } as SignedRequestReplayResource
92
+ const replay = makeResourceSignedRequestReplayStore(resource)
93
+ const expiresAt = new Date(Date.now() + BED255_SIG_TTL)
94
+
95
+ expect(await replay.claim('service:nonce', expiresAt)).toBe(true)
96
+ expect(observedExpiry).toBe(expiresAt)
97
+ expect(await replay.claim('service:nonce', expiresAt)).toBe(false)
98
+ })
99
+ })