@dxos/keys 0.8.4-main.fd6878d → 0.8.4-main.fdfb99ef29

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,9 +1,13 @@
1
1
  {
2
2
  "name": "@dxos/keys",
3
- "version": "0.8.4-main.fd6878d",
3
+ "version": "0.8.4-main.fdfb99ef29",
4
4
  "description": "Key utils and definitions.",
5
5
  "homepage": "https://dxos.org",
6
6
  "bugs": "https://github.com/dxos/dxos/issues",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "https://github.com/dxos/dxos"
10
+ },
7
11
  "license": "MIT",
8
12
  "author": "DXOS.org",
9
13
  "sideEffects": false,
@@ -17,23 +21,23 @@
17
21
  }
18
22
  },
19
23
  "types": "dist/types/src/index.d.ts",
20
- "typesVersions": {
21
- "*": {}
22
- },
23
24
  "files": [
24
25
  "dist",
25
26
  "src"
26
27
  ],
27
28
  "dependencies": {
28
- "effect": "3.17.7",
29
29
  "ulidx": "^2.3.0",
30
- "@dxos/debug": "0.8.4-main.fd6878d",
31
- "@dxos/node-std": "0.8.4-main.fd6878d",
32
- "@dxos/invariant": "0.8.4-main.fd6878d"
30
+ "@dxos/invariant": "0.8.4-main.fdfb99ef29",
31
+ "@dxos/node-std": "0.8.4-main.fdfb99ef29",
32
+ "@dxos/debug": "0.8.4-main.fdfb99ef29"
33
33
  },
34
34
  "devDependencies": {
35
35
  "base32-decode": "^1.0.0",
36
- "base32-encode": "^2.0.0"
36
+ "base32-encode": "^2.0.0",
37
+ "effect": "3.20.0"
38
+ },
39
+ "peerDependencies": {
40
+ "effect": "3.20.0"
37
41
  },
38
42
  "publishConfig": {
39
43
  "access": "public"
package/src/dxn.ts CHANGED
@@ -2,10 +2,9 @@
2
2
  // Copyright 2024 DXOS.org
3
3
  //
4
4
 
5
+ import * as Schema from 'effect/Schema';
5
6
  import type { InspectOptionsStylized, inspect } from 'node:util';
6
7
 
7
- import { Schema } from 'effect';
8
-
9
8
  import { type DevtoolsFormatter, devtoolsFormatter, inspectCustom } from '@dxos/debug';
10
9
  import { assertArgument, invariant } from '@dxos/invariant';
11
10
 
@@ -29,6 +28,12 @@ export const QueueSubspaceTags = Object.freeze({
29
28
 
30
29
  export type QueueSubspaceTag = (typeof QueueSubspaceTags)[keyof typeof QueueSubspaceTags];
31
30
 
31
+ // TODO(burdon): Refactor.
32
+ // Consider: https://github.com/multiformats/multiaddr
33
+ // dxn:echo:[<space-id>:[<queue-id>:]]<object-id>
34
+ // dxn:echo:[S/<space-id>:[Q/<queue-id>:]]<object-id>
35
+ // dxn:type:org.dxos.markdown.contact
36
+
32
37
  /**
33
38
  * DXN unambiguously names a resource like an ECHO object, schema definition, plugin, etc.
34
39
  * Each DXN starts with a dxn prefix, followed by a resource kind.
@@ -41,8 +46,8 @@ export type QueueSubspaceTag = (typeof QueueSubspaceTags)[keyof typeof QueueSubs
41
46
  * dxn:echo:<space key>:<echo id>
42
47
  * dxn:echo:BA25QRC2FEWCSAMRP4RZL65LWJ7352CKE:01J00J9B45YHYSGZQTQMSKMGJ6
43
48
  * dxn:echo:@:01J00J9B45YHYSGZQTQMSKMGJ6
44
- * dxn:type:dxos.org/type/Calendar
45
- * dxn:plugin:dxos.org/agent/plugin/functions
49
+ * dxn:type:org.dxos.type.calendar
50
+ * dxn:plugin:org.dxos.agent.plugin.functions
46
51
  * ```
47
52
  */
48
53
  export class DXN {
@@ -51,11 +56,11 @@ export class DXN {
51
56
  static Schema = Schema.NonEmptyString.pipe(
52
57
  Schema.pattern(/^dxn:([^:]+):(?:[^:]+:?)+[^:]$/),
53
58
  // TODO(dmaretskyi): To set the format we need to move the annotation IDs out of the echo-schema package.
54
- // FormatAnnotation.set(FormatEnum.DXN),
59
+ // FormatAnnotation.set(TypeFormat.DXN),
55
60
  Schema.annotations({
56
61
  title: 'DXN',
57
62
  description: 'DXN URI',
58
- examples: ['dxn:type:example.com/type/MyType', 'dxn:echo:@:01J00J9B45YHYSGZQTQMSKMGJ6'],
63
+ examples: ['dxn:type:com.example.type.my-type', 'dxn:echo:@:01J00J9B45YHYSGZQTQMSKMGJ6'],
59
64
  }),
60
65
  );
61
66
 
@@ -68,16 +73,16 @@ export class DXN {
68
73
  */
69
74
  static kind = Object.freeze({
70
75
  /**
71
- * dxn:type:<type name>[:<version>]
76
+ * dxn:type:<type_name>[:<version>]
72
77
  */
73
78
  TYPE: 'type',
74
79
 
75
80
  /**
76
- * dxn:echo:<space id>:<echo id>
77
- * dxn:echo:@:<echo id>
81
+ * dxn:echo:<space_id>:<echo_id>
82
+ * dxn:echo:@:<echo_id>
78
83
  */
79
- // TODO(burdon): Rename to OBJECT? (BREAKING CHANGE).
80
- // TODO(burdon): Add separate Kind for space.
84
+ // TODO(burdon): Rename to OBJECT? (BREAKING CHANGE to update "echo").
85
+ // TODO(burdon): Add separate Kind for space?
81
86
  ECHO: 'echo',
82
87
 
83
88
  /**
@@ -89,14 +94,19 @@ export class DXN {
89
94
  QUEUE: 'queue',
90
95
  });
91
96
 
92
- get kind() {
93
- return this.#kind;
94
- }
95
-
97
+ /**
98
+ * Exactly equals.
99
+ */
96
100
  static equals(a: DXN, b: DXN): boolean {
97
101
  return a.kind === b.kind && a.parts.length === b.parts.length && a.parts.every((part, i) => part === b.parts[i]);
98
102
  }
99
103
 
104
+ static equalsEchoId(a: DXN, b: DXN): boolean {
105
+ const a1 = a.asEchoDXN();
106
+ const b1 = b.asEchoDXN();
107
+ return !!a1 && !!b1 && a1.echoId === b1.echoId;
108
+ }
109
+
100
110
  // TODO(burdon): Rename isValid.
101
111
  static isDXNString(dxn: string): boolean {
102
112
  return dxn.startsWith('dxn:');
@@ -129,14 +139,14 @@ export class DXN {
129
139
  }
130
140
 
131
141
  /**
132
- * @example `dxn:type:example.com/type/Contact`
142
+ * @example `dxn:type:com.example.type.person`
133
143
  */
134
144
  static fromTypename(typename: string): DXN {
135
145
  return new DXN(DXN.kind.TYPE, [typename]);
136
146
  }
137
147
 
138
148
  /**
139
- * @example `dxn:type:example.com/type/Contact:0.1.0`
149
+ * @example `dxn:type:com.example.type.person:0.1.0`
140
150
  */
141
151
  // TODO(dmaretskyi): Consider using @ as the version separator.
142
152
  static fromTypenameAndVersion(typename: string, version: string): DXN {
@@ -148,7 +158,7 @@ export class DXN {
148
158
  */
149
159
  static fromSpaceAndObjectId(spaceId: SpaceId, objectId: ObjectId): DXN {
150
160
  assertArgument(SpaceId.isValid(spaceId), `Invalid space ID: ${spaceId}`);
151
- assertArgument(ObjectId.isValid(objectId), `Invalid object ID: ${objectId}`);
161
+ assertArgument(ObjectId.isValid(objectId), 'objectId', `Invalid object ID: ${objectId}`);
152
162
  return new DXN(DXN.kind.ECHO, [spaceId, objectId]);
153
163
  }
154
164
 
@@ -156,14 +166,14 @@ export class DXN {
156
166
  * @example `dxn:echo:@:01J00J9B45YHYSGZQTQMSKMGJ6`
157
167
  */
158
168
  static fromLocalObjectId(id: string): DXN {
159
- assertArgument(ObjectId.isValid(id), `Invalid object ID: ${id}`);
169
+ assertArgument(ObjectId.isValid(id), 'id', `Invalid object ID: ${id}`);
160
170
  return new DXN(DXN.kind.ECHO, [LOCAL_SPACE_TAG, id]);
161
171
  }
162
172
 
163
173
  static fromQueue(subspaceTag: QueueSubspaceTag, spaceId: SpaceId, queueId: ObjectId, objectId?: ObjectId) {
164
174
  assertArgument(SpaceId.isValid(spaceId), `Invalid space ID: ${spaceId}`);
165
- assertArgument(ObjectId.isValid(queueId), `Invalid queue ID: ${queueId}`);
166
- assertArgument(!objectId || ObjectId.isValid(objectId), `Invalid object ID: ${objectId}`);
175
+ assertArgument(ObjectId.isValid(queueId), 'queueId', `Invalid queue ID: ${queueId}`);
176
+ assertArgument(!objectId || ObjectId.isValid(objectId), 'objectId', `Invalid object ID: ${objectId}`);
167
177
 
168
178
  return new DXN(DXN.kind.QUEUE, [subspaceTag, spaceId, queueId, ...(objectId ? [objectId] : [])]);
169
179
  }
@@ -172,9 +182,10 @@ export class DXN {
172
182
  #parts: string[];
173
183
 
174
184
  constructor(kind: string, parts: string[]) {
175
- assertArgument(parts.length > 0, `Invalid DXN: ${parts}`);
185
+ assertArgument(parts.length > 0, 'parts', `Invalid DXN: ${parts}`);
176
186
  assertArgument(
177
187
  parts.every((part) => typeof part === 'string' && part.length > 0 && part.indexOf(':') === -1),
188
+ 'parts',
178
189
  `Invalid DXN: ${parts}`,
179
190
  );
180
191
 
@@ -182,12 +193,12 @@ export class DXN {
182
193
  switch (kind) {
183
194
  case DXN.kind.TYPE:
184
195
  if (parts.length > 2) {
185
- throw new Error('Invalid "type" DXN');
196
+ throw new Error('Invalid DXN.kind.TYPE');
186
197
  }
187
198
  break;
188
199
  case DXN.kind.ECHO:
189
200
  if (parts.length !== 2) {
190
- throw new Error('Invalid "echo" DXN');
201
+ throw new Error('Invalid DXN.kind.ECHO');
191
202
  }
192
203
  break;
193
204
  }
@@ -225,6 +236,10 @@ export class DXN {
225
236
  };
226
237
  }
227
238
 
239
+ get kind() {
240
+ return this.#kind;
241
+ }
242
+
228
243
  get parts() {
229
244
  return this.#parts;
230
245
  }
@@ -235,6 +250,10 @@ export class DXN {
235
250
  return this.#parts[0];
236
251
  }
237
252
 
253
+ equals(other: DXN): boolean {
254
+ return DXN.equals(this, other);
255
+ }
256
+
238
257
  hasTypenameOf(typename: string): boolean {
239
258
  return this.#kind === DXN.kind.TYPE && this.#parts.length === 1 && this.#parts[0] === typename;
240
259
  }
@@ -264,6 +283,7 @@ export class DXN {
264
283
  const [spaceId, echoId] = this.#parts;
265
284
  return {
266
285
  spaceId: spaceId === LOCAL_SPACE_TAG ? undefined : (spaceId as SpaceId | undefined),
286
+ // TODO(burdon): objectId.
267
287
  echoId,
268
288
  };
269
289
  }
@@ -279,7 +299,7 @@ export class DXN {
279
299
  }
280
300
 
281
301
  return {
282
- subspaceTag,
302
+ subspaceTag: subspaceTag as QueueSubspaceTag,
283
303
  spaceId: spaceId as SpaceId,
284
304
  queueId,
285
305
  objectId: objectId as string | undefined,
@@ -294,33 +314,18 @@ export class DXN {
294
314
  }
295
315
  }
296
316
 
297
- // TODO(dmaretskyi): Fluent API:
298
- /*
299
- class DXN {
300
- ...
301
- isEchoDXN(): this is EchoDXN {
302
- return this.#kind === DXN.kind.ECHO;
303
- }
304
- ...
305
- }
306
-
307
- interface EchoDXN extends DXN {
308
- objectId: ObjectId;
309
- }
310
-
311
- declare const dxn: DXN;
312
-
313
- dxn.objectId
314
-
315
- if(dxn.isEchoDXN()) {
316
- dxn.objectId
317
- }
318
- ```
319
-
320
317
  /**
321
318
  * API namespace.
322
319
  */
323
320
  export declare namespace DXN {
321
+ /**
322
+ * DXN represented as a javascript string.
323
+ */
324
+ // TODO(burdon): Use Effect branded string?
325
+ // export const String = S.String.pipe(S.brand('DXN'));
326
+ // export type String = S.To(typoeof String);
327
+ export type String = string & { __DXNString: never };
328
+
324
329
  export type TypeDXN = {
325
330
  type: string;
326
331
  version?: string;
@@ -332,17 +337,9 @@ export declare namespace DXN {
332
337
  };
333
338
 
334
339
  export type QueueDXN = {
335
- subspaceTag: string;
340
+ subspaceTag: QueueSubspaceTag;
336
341
  spaceId: SpaceId;
337
342
  queueId: string; // TODO(dmaretskyi): ObjectId.
338
343
  objectId?: string; // TODO(dmaretskyi): ObjectId.
339
344
  };
340
-
341
- /**
342
- * DXN represented as a javascript string.
343
- */
344
- export type String = string & { __DXNString: never };
345
- // TODO(burdon): Make brand.
346
- // export const String = S.String.pipe(S.brand('DXN'));
347
- // export type String = S.To(typoeof String);
348
345
  }
package/src/object-id.ts CHANGED
@@ -2,8 +2,8 @@
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');
@@ -16,9 +16,39 @@ const ObjectIdSchema = Schema.String.pipe(Schema.pattern(/^[0-7][0-9A-HJKMNP-TV-
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,6 +57,9 @@ 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);
@@ -37,6 +70,62 @@ export const ObjectId: ObjectIdClass = class extends ObjectIdSchema {
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,10 +2,9 @@
2
2
  // Copyright 2020 DXOS.org
3
3
  //
4
4
 
5
- import { type InspectOptionsStylized, type inspect } from 'node:util';
6
-
7
5
  import base32Decode from 'base32-decode';
8
6
  import base32Encode from 'base32-encode';
7
+ import { type InspectOptionsStylized, type inspect } from 'node:util';
9
8
 
10
9
  import {
11
10
  type DevtoolsFormatter,
@@ -217,7 +216,7 @@ export class PublicKey implements Equatable {
217
216
  return 'B' + base32Encode(this._value, 'RFC4648');
218
217
  }
219
218
 
220
- truncate(length = undefined): string {
219
+ truncate(length?: number): string {
221
220
  return truncateKey(this, length);
222
221
  }
223
222
 
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
 
@@ -17,7 +17,7 @@ const MULTIBASE_PREFIX = 'B';
17
17
 
18
18
  const ENCODED_LENGTH = 33;
19
19
 
20
- const isValid = (value: string): value is SpaceId => {
20
+ const isValid = (value: unknown): value is SpaceId => {
21
21
  return typeof value === 'string' && value.startsWith(MULTIBASE_PREFIX) && value.length === ENCODED_LENGTH;
22
22
  };
23
23
 
@@ -33,7 +33,7 @@ export const SpaceId: Schema.Schema<SpaceId, string> & {
33
33
  byteLength: number;
34
34
  encode: (value: Uint8Array) => SpaceId;
35
35
  decode: (value: SpaceId) => Uint8Array;
36
- isValid: (value: string) => value is SpaceId;
36
+ isValid: (value: unknown) => value is SpaceId;
37
37
  make: (value: string) => SpaceId;
38
38
  random: () => SpaceId;
39
39
  } = class extends Schema.String.pipe(Schema.filter(isValid)) {