@dxos/keys 0.8.4-main.84f28bd → 0.8.4-main.ae835ea

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dxos/keys",
3
- "version": "0.8.4-main.84f28bd",
3
+ "version": "0.8.4-main.ae835ea",
4
4
  "description": "Key utils and definitions.",
5
5
  "homepage": "https://dxos.org",
6
6
  "bugs": "https://github.com/dxos/dxos/issues",
@@ -10,12 +10,10 @@
10
10
  "type": "module",
11
11
  "exports": {
12
12
  ".": {
13
+ "source": "./src/index.ts",
14
+ "types": "./dist/types/src/index.d.ts",
13
15
  "browser": "./dist/lib/browser/index.mjs",
14
- "node": {
15
- "require": "./dist/lib/node/index.cjs",
16
- "default": "./dist/lib/node-esm/index.mjs"
17
- },
18
- "types": "./dist/types/src/index.d.ts"
16
+ "node": "./dist/lib/node-esm/index.mjs"
19
17
  }
20
18
  },
21
19
  "types": "dist/types/src/index.d.ts",
@@ -27,11 +25,11 @@
27
25
  "src"
28
26
  ],
29
27
  "dependencies": {
30
- "effect": "3.16.13",
28
+ "effect": "3.18.3",
31
29
  "ulidx": "^2.3.0",
32
- "@dxos/invariant": "0.8.4-main.84f28bd",
33
- "@dxos/debug": "0.8.4-main.84f28bd",
34
- "@dxos/node-std": "0.8.4-main.84f28bd"
30
+ "@dxos/debug": "0.8.4-main.ae835ea",
31
+ "@dxos/invariant": "0.8.4-main.ae835ea",
32
+ "@dxos/node-std": "0.8.4-main.ae835ea"
35
33
  },
36
34
  "devDependencies": {
37
35
  "base32-decode": "^1.0.0",
package/src/dxn.ts CHANGED
@@ -2,11 +2,12 @@
2
2
  // Copyright 2024 DXOS.org
3
3
  //
4
4
 
5
- import { Schema } from 'effect';
6
- import type { inspect, InspectOptionsStylized } from 'node:util';
5
+ import type { InspectOptionsStylized, inspect } from 'node:util';
7
6
 
8
- import { devtoolsFormatter, type DevtoolsFormatter, inspectCustom } from '@dxos/debug';
9
- import { invariant } from '@dxos/invariant';
7
+ import * as Schema from 'effect/Schema';
8
+
9
+ import { type DevtoolsFormatter, devtoolsFormatter, inspectCustom } from '@dxos/debug';
10
+ import { assertArgument, invariant } from '@dxos/invariant';
10
11
 
11
12
  import { ObjectId } from './object-id';
12
13
  import { SpaceId } from './space-id';
@@ -18,6 +19,8 @@ import { SpaceId } from './space-id';
18
19
  // TODO(dmaretskyi): "@" is a separator character in the URI spec.
19
20
  export const LOCAL_SPACE_TAG = '@';
20
21
 
22
+ export const DXN_ECHO_REGEXP = /@(dxn:[a-zA-Z0-p:@]+)/;
23
+
21
24
  // TODO(burdon): Namespace for.
22
25
  export const QueueSubspaceTags = Object.freeze({
23
26
  DATA: 'data',
@@ -26,6 +29,12 @@ export const QueueSubspaceTags = Object.freeze({
26
29
 
27
30
  export type QueueSubspaceTag = (typeof QueueSubspaceTags)[keyof typeof QueueSubspaceTags];
28
31
 
32
+ // TODO(burdon): Refactor.
33
+ // Consider: https://github.com/multiformats/multiaddr
34
+ // dxn:echo:[<space-id>:[<queue-id>:]]<object-id>
35
+ // dxn:echo:[S/<space-id>:[Q/<queue-id>:]]<object-id>
36
+ // dxn:type:dxos.org/markdown/Contact
37
+
29
38
  /**
30
39
  * DXN unambiguously names a resource like an ECHO object, schema definition, plugin, etc.
31
40
  * Each DXN starts with a dxn prefix, followed by a resource kind.
@@ -43,6 +52,7 @@ export type QueueSubspaceTag = (typeof QueueSubspaceTags)[keyof typeof QueueSubs
43
52
  * ```
44
53
  */
45
54
  export class DXN {
55
+ // TODO(burdon): Rename to DXN (i.e., DXN.DXN).
46
56
  // TODO(dmaretskyi): Should this be a transformation into the DXN type?
47
57
  static Schema = Schema.NonEmptyString.pipe(
48
58
  Schema.pattern(/^dxn:([^:]+):(?:[^:]+:?)+[^:]$/),
@@ -64,16 +74,16 @@ export class DXN {
64
74
  */
65
75
  static kind = Object.freeze({
66
76
  /**
67
- * dxn:type:<type name>[:<version>]
77
+ * dxn:type:<type_name>[:<version>]
68
78
  */
69
79
  TYPE: 'type',
70
80
 
71
81
  /**
72
- * dxn:echo:<space id>:<echo id>
73
- * dxn:echo:@:<echo id>
82
+ * dxn:echo:<space_id>:<echo_id>
83
+ * dxn:echo:@:<echo_id>
74
84
  */
75
- // TODO(burdon): Rename to OBJECT? (BREAKING CHANGE).
76
- // TODO(burdon): Add separate Kind for space.
85
+ // TODO(burdon): Rename to OBJECT? (BREAKING CHANGE to update "echo").
86
+ // TODO(burdon): Add separate Kind for space?
77
87
  ECHO: 'echo',
78
88
 
79
89
  /**
@@ -85,14 +95,19 @@ export class DXN {
85
95
  QUEUE: 'queue',
86
96
  });
87
97
 
88
- get kind() {
89
- return this.#kind;
90
- }
91
-
98
+ /**
99
+ * Exactly equals.
100
+ */
92
101
  static equals(a: DXN, b: DXN): boolean {
93
102
  return a.kind === b.kind && a.parts.length === b.parts.length && a.parts.every((part, i) => part === b.parts[i]);
94
103
  }
95
104
 
105
+ static equalsEchoId(a: DXN, b: DXN): boolean {
106
+ const a1 = a.asEchoDXN();
107
+ const b1 = b.asEchoDXN();
108
+ return !!a1 && !!b1 && a1.echoId === b1.echoId;
109
+ }
110
+
96
111
  // TODO(burdon): Rename isValid.
97
112
  static isDXNString(dxn: string): boolean {
98
113
  return dxn.startsWith('dxn:');
@@ -119,7 +134,7 @@ export class DXN {
119
134
  static tryParse(dxn: string): DXN | undefined {
120
135
  try {
121
136
  return DXN.parse(dxn);
122
- } catch (error) {
137
+ } catch {
123
138
  return undefined;
124
139
  }
125
140
  }
@@ -139,17 +154,27 @@ export class DXN {
139
154
  return new DXN(DXN.kind.TYPE, [typename, version]);
140
155
  }
141
156
 
157
+ /**
158
+ * @example `dxn:echo:BA25QRC2FEWCSAMRP4RZL65LWJ7352CKE:01J00J9B45YHYSGZQTQMSKMGJ6`
159
+ */
160
+ static fromSpaceAndObjectId(spaceId: SpaceId, objectId: ObjectId): DXN {
161
+ assertArgument(SpaceId.isValid(spaceId), `Invalid space ID: ${spaceId}`);
162
+ assertArgument(ObjectId.isValid(objectId), 'objectId', `Invalid object ID: ${objectId}`);
163
+ return new DXN(DXN.kind.ECHO, [spaceId, objectId]);
164
+ }
165
+
142
166
  /**
143
167
  * @example `dxn:echo:@:01J00J9B45YHYSGZQTQMSKMGJ6`
144
168
  */
145
169
  static fromLocalObjectId(id: string): DXN {
170
+ assertArgument(ObjectId.isValid(id), 'id', `Invalid object ID: ${id}`);
146
171
  return new DXN(DXN.kind.ECHO, [LOCAL_SPACE_TAG, id]);
147
172
  }
148
173
 
149
174
  static fromQueue(subspaceTag: QueueSubspaceTag, spaceId: SpaceId, queueId: ObjectId, objectId?: ObjectId) {
150
- invariant(SpaceId.isValid(spaceId));
151
- invariant(ObjectId.isValid(queueId));
152
- invariant(!objectId || ObjectId.isValid(objectId));
175
+ assertArgument(SpaceId.isValid(spaceId), `Invalid space ID: ${spaceId}`);
176
+ assertArgument(ObjectId.isValid(queueId), 'queueId', `Invalid queue ID: ${queueId}`);
177
+ assertArgument(!objectId || ObjectId.isValid(objectId), 'objectId', `Invalid object ID: ${objectId}`);
153
178
 
154
179
  return new DXN(DXN.kind.QUEUE, [subspaceTag, spaceId, queueId, ...(objectId ? [objectId] : [])]);
155
180
  }
@@ -158,19 +183,23 @@ export class DXN {
158
183
  #parts: string[];
159
184
 
160
185
  constructor(kind: string, parts: string[]) {
161
- invariant(parts.length > 0);
162
- invariant(parts.every((part) => typeof part === 'string' && part.length > 0 && part.indexOf(':') === -1));
186
+ assertArgument(parts.length > 0, 'parts', `Invalid DXN: ${parts}`);
187
+ assertArgument(
188
+ parts.every((part) => typeof part === 'string' && part.length > 0 && part.indexOf(':') === -1),
189
+ 'parts',
190
+ `Invalid DXN: ${parts}`,
191
+ );
163
192
 
164
193
  // Per-type validation.
165
194
  switch (kind) {
166
195
  case DXN.kind.TYPE:
167
196
  if (parts.length > 2) {
168
- throw new Error('Invalid "type" DXN');
197
+ throw new Error('Invalid DXN.kind.TYPE');
169
198
  }
170
199
  break;
171
200
  case DXN.kind.ECHO:
172
201
  if (parts.length !== 2) {
173
- throw new Error('Invalid "echo" DXN');
202
+ throw new Error('Invalid DXN.kind.ECHO');
174
203
  }
175
204
  break;
176
205
  }
@@ -208,6 +237,10 @@ export class DXN {
208
237
  };
209
238
  }
210
239
 
240
+ get kind() {
241
+ return this.#kind;
242
+ }
243
+
211
244
  get parts() {
212
245
  return this.#parts;
213
246
  }
@@ -218,6 +251,10 @@ export class DXN {
218
251
  return this.#parts[0];
219
252
  }
220
253
 
254
+ equals(other: DXN): boolean {
255
+ return DXN.equals(this, other);
256
+ }
257
+
221
258
  hasTypenameOf(typename: string): boolean {
222
259
  return this.#kind === DXN.kind.TYPE && this.#parts.length === 1 && this.#parts[0] === typename;
223
260
  }
@@ -233,6 +270,7 @@ export class DXN {
233
270
 
234
271
  const [type, version] = this.#parts;
235
272
  return {
273
+ // TODO(wittjosiah): Should be `typename` for consistency.
236
274
  type,
237
275
  version: version as string | undefined,
238
276
  };
@@ -246,6 +284,7 @@ export class DXN {
246
284
  const [spaceId, echoId] = this.#parts;
247
285
  return {
248
286
  spaceId: spaceId === LOCAL_SPACE_TAG ? undefined : (spaceId as SpaceId | undefined),
287
+ // TODO(burdon): objectId.
249
288
  echoId,
250
289
  };
251
290
  }
@@ -267,35 +306,27 @@ export class DXN {
267
306
  objectId: objectId as string | undefined,
268
307
  };
269
308
  }
270
- }
271
309
 
272
- // TODO(dmaretskyi): Fluent API:
273
- /*
274
- class DXN {
275
- ...
276
- isEchoDXN(): this is EchoDXN {
277
- return this.#kind === DXN.kind.ECHO;
278
- }
279
- ...
280
- }
281
-
282
- interface EchoDXN extends DXN {
283
- objectId: ObjectId;
284
- }
285
-
286
- declare const dxn: DXN;
287
-
288
- dxn.objectId
289
-
290
- if(dxn.isEchoDXN()) {
291
- dxn.objectId
310
+ /**
311
+ * Produces a new DXN with the given parts appended.
312
+ */
313
+ extend(parts: string[]): DXN {
314
+ return new DXN(this.#kind, [...this.#parts, ...parts]);
315
+ }
292
316
  }
293
- ```
294
317
 
295
318
  /**
296
319
  * API namespace.
297
320
  */
298
321
  export declare namespace DXN {
322
+ /**
323
+ * DXN represented as a javascript string.
324
+ */
325
+ // TODO(burdon): Use Effect branded string?
326
+ // export const String = S.String.pipe(S.brand('DXN'));
327
+ // export type String = S.To(typoeof String);
328
+ export type String = string & { __DXNString: never };
329
+
299
330
  export type TypeDXN = {
300
331
  type: string;
301
332
  version?: string;
@@ -303,8 +334,7 @@ export declare namespace DXN {
303
334
 
304
335
  export type EchoDXN = {
305
336
  spaceId?: SpaceId;
306
- // TODO(burdon): Rename objectId.
307
- echoId: string; // TODO(dmaretskyi): ObjectId.
337
+ echoId: string; // TODO(dmaretskyi): Rename to `objectId` and use `ObjectId` for the type.
308
338
  };
309
339
 
310
340
  export type QueueDXN = {
@@ -313,12 +343,4 @@ export declare namespace DXN {
313
343
  queueId: string; // TODO(dmaretskyi): ObjectId.
314
344
  objectId?: string; // TODO(dmaretskyi): ObjectId.
315
345
  };
316
-
317
- /**
318
- * DXN represented as a javascript string.
319
- */
320
- export type String = string & { __DXNString: never };
321
- // TODO(burdon): Make brand.
322
- // export const String = S.String.pipe(S.brand('DXN'));
323
- // export type String = S.To(typoeof String);
324
346
  }
package/src/index.ts CHANGED
@@ -7,4 +7,4 @@ export * from './identity-did';
7
7
  export * from './object-id';
8
8
  export * from './public-key';
9
9
  export * from './space-id';
10
- export * from './types';
10
+ export type * from './types';
package/src/object-id.ts CHANGED
@@ -2,23 +2,53 @@
2
2
  // Copyright 2025 DXOS.org
3
3
  //
4
4
 
5
- import { Schema } from 'effect';
6
- import { ulid } from 'ulidx';
5
+ import * as Schema from 'effect/Schema';
6
+ import { type PRNG, type ULIDFactory, monotonicFactory } from 'ulidx';
7
7
 
8
8
  // TODO(dmaretskyi): Make brand.
9
9
  // export const ObjectIdBrand: unique symbol = Symbol('@dxos/echo/ObjectId');
10
10
  // export const ObjectIdSchema = Schema.ULID.pipe(S.brand(ObjectIdBrand));
11
11
  const ObjectIdSchema = Schema.String.pipe(Schema.pattern(/^[0-7][0-9A-HJKMNP-TV-Z]{25}$/i)).annotations({
12
- description: 'a Universally Unique Lexicographically Sortable Identifier',
12
+ description: 'A Universally Unique Lexicographically Sortable Identifier',
13
13
  pattern: '^[0-7][0-9A-HJKMNP-TV-Z]{25}$',
14
14
  });
15
15
 
16
16
  export type ObjectId = typeof ObjectIdSchema.Type;
17
17
 
18
18
  export interface ObjectIdClass extends Schema.SchemaClass<ObjectId, string> {
19
+ /**
20
+ * @returns true if the string is a valid ObjectId.
21
+ */
19
22
  isValid(id: string): id is ObjectId;
23
+
24
+ /**
25
+ * Creates an ObjectId from a string validating the format.
26
+ */
20
27
  make(id: string): ObjectId;
28
+
29
+ /**
30
+ * Generates a random ObjectId.
31
+ */
21
32
  random(): ObjectId;
33
+
34
+ /**
35
+ * WARNING: To be used only within tests.
36
+ *
37
+ * Disables randomness in ObjectId generation, causing the same sequence of IDs to be generated.
38
+ * Do not use in production code as this will cause data collisions.
39
+ * Place this at the top of the test file to ensure that the same sequence of IDs is generated.
40
+ *
41
+ * ```ts
42
+ * ObjectId.dangerouslyDisableRandomness();
43
+ *
44
+ * describe('suite', () => {
45
+ * // ...
46
+ * });
47
+ * ```
48
+ *
49
+ * NOTE: The generated IDs depend on the order of ObjectId.random() calls, which might be affected by test order, scheduling, etc.
50
+ */
51
+ dangerouslyDisableRandomness(): void;
22
52
  }
23
53
 
24
54
  /**
@@ -27,16 +57,75 @@ export interface ObjectIdClass extends Schema.SchemaClass<ObjectId, string> {
27
57
  * Follows ULID spec.
28
58
  */
29
59
  export const ObjectId: ObjectIdClass = class extends ObjectIdSchema {
60
+ static #factory: ULIDFactory = monotonicFactory();
61
+ static #seedTime: number | undefined = undefined;
62
+
30
63
  static isValid(id: string): id is ObjectId {
31
64
  try {
32
65
  Schema.decodeSync(ObjectId)(id);
33
66
  return true;
34
- } catch (err) {
67
+ } catch {
35
68
  return false;
36
69
  }
37
70
  }
38
71
 
39
72
  static random(): ObjectId {
40
- return ulid() as ObjectId;
73
+ return this.#factory(this.#seedTime) as ObjectId;
74
+ }
75
+
76
+ static dangerouslyDisableRandomness() {
77
+ this.#factory = monotonicFactory(makeTestPRNG());
78
+ this.#seedTime = new Date('2025-01-01').getTime();
41
79
  }
42
80
  };
81
+
82
+ /**
83
+ * Test PRNG that always starts with the same seed and produces the same sequence.
84
+ */
85
+ const makeTestPRNG = (): PRNG => {
86
+ const rng = new SimplePRNG();
87
+ return () => {
88
+ return rng.next();
89
+ };
90
+ };
91
+
92
+ /**
93
+ * Simple Linear Congruential Generator (LCG) for pseudo-random number generation.
94
+ * Returns numbers in the range [0, 1) (0 inclusive, 1 exclusive).
95
+ */
96
+ export class SimplePRNG {
97
+ #seed: number;
98
+
99
+ // LCG parameters (from Numerical Recipes)
100
+ static readonly #a = 1664525;
101
+ static readonly #c = 1013904223;
102
+ static readonly #m = Math.pow(2, 32);
103
+
104
+ /**
105
+ * Creates a new PRNG instance.
106
+ * @param seed - Initial seed value. If not provided, uses 0.
107
+ */
108
+ constructor(seed: number = 0) {
109
+ this.#seed = seed;
110
+ }
111
+
112
+ /**
113
+ * Generates the next pseudo-random number in the range [0, 1).
114
+ * @returns A pseudo-random number between 0 (inclusive) and 1 (exclusive).
115
+ */
116
+ next(): number {
117
+ // Update seed using LCG formula: (a * seed + c) mod m
118
+ this.#seed = (SimplePRNG.#a * this.#seed + SimplePRNG.#c) % SimplePRNG.#m;
119
+
120
+ // Normalize to [0, 1) range
121
+ return this.#seed / SimplePRNG.#m;
122
+ }
123
+
124
+ /**
125
+ * Resets the generator with a new seed.
126
+ * @param seed - New seed value.
127
+ */
128
+ reset(seed: number): void {
129
+ this.#seed = seed;
130
+ }
131
+ }
package/src/prng.ts ADDED
@@ -0,0 +1,54 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ /**
6
+ * Simple Linear Congruential Generator (LCG) for pseudo-random number generation.
7
+ * Returns numbers in the range [0, 1) (0 inclusive, 1 exclusive).
8
+ */
9
+ export class SimplePRNG {
10
+ private _seed: number;
11
+
12
+ // LCG parameters (from Numerical Recipes)
13
+ private static readonly _a = 1664525;
14
+ private static readonly _c = 1013904223;
15
+ private static readonly _m = Math.pow(2, 32);
16
+
17
+ /**
18
+ * Creates a new PRNG instance.
19
+ * @param seed - Initial seed value. If not provided, uses current timestamp.
20
+ */
21
+ constructor(seed?: number) {
22
+ this._seed = seed ?? Date.now();
23
+ }
24
+
25
+ /**
26
+ * Generates the next pseudo-random number in the range [0, 1).
27
+ * @returns A pseudo-random number between 0 (inclusive) and 1 (exclusive).
28
+ */
29
+ next(): number {
30
+ // Update seed using LCG formula: (a * seed + c) mod m
31
+ this._seed = (SimplePRNG._a * this._seed + SimplePRNG._c) % SimplePRNG._m;
32
+
33
+ // Normalize to [0, 1) range
34
+ return this._seed / SimplePRNG._m;
35
+ }
36
+
37
+ /**
38
+ * Resets the generator with a new seed.
39
+ * @param seed - New seed value.
40
+ */
41
+ reset(seed: number): void {
42
+ this._seed = seed;
43
+ }
44
+ }
45
+
46
+ /**
47
+ * Creates a simple PRNG function with optional seed.
48
+ * @param seed - Optional seed value.
49
+ * @returns A function that returns pseudo-random numbers in [0, 1).
50
+ */
51
+ export const createPRNG = (seed?: number): (() => number) => {
52
+ const prng = new SimplePRNG(seed);
53
+ return () => prng.next();
54
+ };
package/src/public-key.ts CHANGED
@@ -2,15 +2,16 @@
2
2
  // Copyright 2020 DXOS.org
3
3
  //
4
4
 
5
+ import { type InspectOptionsStylized, type inspect } from 'node:util';
6
+
5
7
  import base32Decode from 'base32-decode';
6
8
  import base32Encode from 'base32-encode';
7
- import { type inspect, type InspectOptionsStylized } from 'node:util';
8
9
 
9
10
  import {
10
- devtoolsFormatter,
11
11
  type DevtoolsFormatter,
12
- equalsSymbol,
13
12
  type Equatable,
13
+ devtoolsFormatter,
14
+ equalsSymbol,
14
15
  inspectCustom,
15
16
  truncateKey,
16
17
  } from '@dxos/debug';
@@ -4,7 +4,7 @@
4
4
 
5
5
  export const randomBytes = (length: number) => {
6
6
  // globalThis.crypto is not available in Node.js when running in vitest even though the documentation says it should be.
7
- // eslint-disable-next-line @typescript-eslint/no-var-requires
7
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
8
8
  const webCrypto = globalThis.crypto ?? require('node:crypto').webcrypto;
9
9
 
10
10
  const bytes = new Uint8Array(length);
package/src/space-id.ts CHANGED
@@ -4,7 +4,7 @@
4
4
 
5
5
  import base32Decode from 'base32-decode';
6
6
  import base32Encode from 'base32-encode';
7
- import { Schema } from 'effect';
7
+ import * as Schema from 'effect/Schema';
8
8
 
9
9
  import { invariant } from '@dxos/invariant';
10
10