@dxos/keys 0.8.4-main.84f28bd → 0.8.4-main.8baae0fced
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/LICENSE +102 -5
- package/dist/lib/browser/index.mjs +196 -229
- package/dist/lib/browser/index.mjs.map +4 -4
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/node-esm/index.mjs +195 -229
- package/dist/lib/node-esm/index.mjs.map +4 -4
- package/dist/lib/node-esm/meta.json +1 -1
- package/dist/types/src/dxn.d.ts +35 -18
- package/dist/types/src/dxn.d.ts.map +1 -1
- package/dist/types/src/index.d.ts +2 -1
- package/dist/types/src/index.d.ts.map +1 -1
- package/dist/types/src/object-id.d.ts +67 -1
- package/dist/types/src/object-id.d.ts.map +1 -1
- package/dist/types/src/parse-id.d.ts +10 -0
- package/dist/types/src/parse-id.d.ts.map +1 -0
- package/dist/types/src/parse-id.test.d.ts +2 -0
- package/dist/types/src/parse-id.test.d.ts.map +1 -0
- package/dist/types/src/prng.d.ts +32 -0
- package/dist/types/src/prng.d.ts.map +1 -0
- package/dist/types/src/public-key.d.ts +3 -3
- package/dist/types/src/public-key.d.ts.map +1 -1
- package/dist/types/src/random-bytes.d.ts.map +1 -1
- package/dist/types/src/space-id.d.ts +2 -2
- package/dist/types/src/space-id.d.ts.map +1 -1
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/package.json +17 -15
- package/src/dxn.ts +82 -61
- package/src/index.ts +2 -1
- package/src/object-id.ts +117 -5
- package/src/parse-id.test.ts +32 -0
- package/src/parse-id.ts +32 -0
- package/src/prng.ts +54 -0
- package/src/public-key.ts +4 -4
- package/src/random-bytes.ts +1 -1
- package/src/space-id.ts +3 -3
package/package.json
CHANGED
|
@@ -1,41 +1,43 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dxos/keys",
|
|
3
|
-
"version": "0.8.4-main.
|
|
3
|
+
"version": "0.8.4-main.8baae0fced",
|
|
4
4
|
"description": "Key utils and definitions.",
|
|
5
5
|
"homepage": "https://dxos.org",
|
|
6
6
|
"bugs": "https://github.com/dxos/dxos/issues",
|
|
7
|
-
"
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "https://github.com/dxos/dxos"
|
|
10
|
+
},
|
|
11
|
+
"license": "FSL-1.1-Apache-2.0",
|
|
8
12
|
"author": "DXOS.org",
|
|
9
13
|
"sideEffects": false,
|
|
10
14
|
"type": "module",
|
|
11
15
|
"exports": {
|
|
12
16
|
".": {
|
|
17
|
+
"source": "./src/index.ts",
|
|
18
|
+
"types": "./dist/types/src/index.d.ts",
|
|
13
19
|
"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"
|
|
20
|
+
"node": "./dist/lib/node-esm/index.mjs"
|
|
19
21
|
}
|
|
20
22
|
},
|
|
21
23
|
"types": "dist/types/src/index.d.ts",
|
|
22
|
-
"typesVersions": {
|
|
23
|
-
"*": {}
|
|
24
|
-
},
|
|
25
24
|
"files": [
|
|
26
25
|
"dist",
|
|
27
26
|
"src"
|
|
28
27
|
],
|
|
29
28
|
"dependencies": {
|
|
30
|
-
"effect": "3.16.13",
|
|
31
29
|
"ulidx": "^2.3.0",
|
|
32
|
-
"@dxos/
|
|
33
|
-
"@dxos/
|
|
34
|
-
"@dxos/node-std": "0.8.4-main.
|
|
30
|
+
"@dxos/debug": "0.8.4-main.8baae0fced",
|
|
31
|
+
"@dxos/invariant": "0.8.4-main.8baae0fced",
|
|
32
|
+
"@dxos/node-std": "0.8.4-main.8baae0fced"
|
|
35
33
|
},
|
|
36
34
|
"devDependencies": {
|
|
37
35
|
"base32-decode": "^1.0.0",
|
|
38
|
-
"base32-encode": "^2.0.0"
|
|
36
|
+
"base32-encode": "^2.0.0",
|
|
37
|
+
"effect": "3.20.0"
|
|
38
|
+
},
|
|
39
|
+
"peerDependencies": {
|
|
40
|
+
"effect": "3.20.0"
|
|
39
41
|
},
|
|
40
42
|
"publishConfig": {
|
|
41
43
|
"access": "public"
|
package/src/dxn.ts
CHANGED
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
// Copyright 2024 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import
|
|
6
|
-
import type {
|
|
5
|
+
import * as Schema from 'effect/Schema';
|
|
6
|
+
import type { InspectOptionsStylized, inspect } from 'node:util';
|
|
7
7
|
|
|
8
|
-
import {
|
|
9
|
-
import { invariant } from '@dxos/invariant';
|
|
8
|
+
import { type DevtoolsFormatter, devtoolsFormatter, inspectCustom } from '@dxos/debug';
|
|
9
|
+
import { assertArgument, invariant } from '@dxos/invariant';
|
|
10
10
|
|
|
11
11
|
import { ObjectId } from './object-id';
|
|
12
12
|
import { SpaceId } from './space-id';
|
|
@@ -18,6 +18,8 @@ import { SpaceId } from './space-id';
|
|
|
18
18
|
// TODO(dmaretskyi): "@" is a separator character in the URI spec.
|
|
19
19
|
export const LOCAL_SPACE_TAG = '@';
|
|
20
20
|
|
|
21
|
+
export const DXN_ECHO_REGEXP = /@(dxn:[a-zA-Z0-p:@]+)/;
|
|
22
|
+
|
|
21
23
|
// TODO(burdon): Namespace for.
|
|
22
24
|
export const QueueSubspaceTags = Object.freeze({
|
|
23
25
|
DATA: 'data',
|
|
@@ -26,6 +28,12 @@ export const QueueSubspaceTags = Object.freeze({
|
|
|
26
28
|
|
|
27
29
|
export type QueueSubspaceTag = (typeof QueueSubspaceTags)[keyof typeof QueueSubspaceTags];
|
|
28
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
|
+
|
|
29
37
|
/**
|
|
30
38
|
* DXN unambiguously names a resource like an ECHO object, schema definition, plugin, etc.
|
|
31
39
|
* Each DXN starts with a dxn prefix, followed by a resource kind.
|
|
@@ -38,20 +46,21 @@ export type QueueSubspaceTag = (typeof QueueSubspaceTags)[keyof typeof QueueSubs
|
|
|
38
46
|
* dxn:echo:<space key>:<echo id>
|
|
39
47
|
* dxn:echo:BA25QRC2FEWCSAMRP4RZL65LWJ7352CKE:01J00J9B45YHYSGZQTQMSKMGJ6
|
|
40
48
|
* dxn:echo:@:01J00J9B45YHYSGZQTQMSKMGJ6
|
|
41
|
-
* dxn:type:dxos.
|
|
42
|
-
* dxn:plugin:dxos.
|
|
49
|
+
* dxn:type:org.dxos.type.calendar
|
|
50
|
+
* dxn:plugin:org.dxos.agent.plugin.functions
|
|
43
51
|
* ```
|
|
44
52
|
*/
|
|
45
53
|
export class DXN {
|
|
54
|
+
// TODO(burdon): Rename to DXN (i.e., DXN.DXN).
|
|
46
55
|
// TODO(dmaretskyi): Should this be a transformation into the DXN type?
|
|
47
56
|
static Schema = Schema.NonEmptyString.pipe(
|
|
48
57
|
Schema.pattern(/^dxn:([^:]+):(?:[^:]+:?)+[^:]$/),
|
|
49
58
|
// TODO(dmaretskyi): To set the format we need to move the annotation IDs out of the echo-schema package.
|
|
50
|
-
// FormatAnnotation.set(
|
|
59
|
+
// FormatAnnotation.set(TypeFormat.DXN),
|
|
51
60
|
Schema.annotations({
|
|
52
61
|
title: 'DXN',
|
|
53
62
|
description: 'DXN URI',
|
|
54
|
-
examples: ['dxn:type:example.
|
|
63
|
+
examples: ['dxn:type:com.example.type.my-type', 'dxn:echo:@:01J00J9B45YHYSGZQTQMSKMGJ6'],
|
|
55
64
|
}),
|
|
56
65
|
);
|
|
57
66
|
|
|
@@ -64,16 +73,16 @@ export class DXN {
|
|
|
64
73
|
*/
|
|
65
74
|
static kind = Object.freeze({
|
|
66
75
|
/**
|
|
67
|
-
* dxn:type:<
|
|
76
|
+
* dxn:type:<type_name>[:<version>]
|
|
68
77
|
*/
|
|
69
78
|
TYPE: 'type',
|
|
70
79
|
|
|
71
80
|
/**
|
|
72
|
-
* dxn:echo:<
|
|
73
|
-
* dxn:echo:@:<
|
|
81
|
+
* dxn:echo:<space_id>:<echo_id>
|
|
82
|
+
* dxn:echo:@:<echo_id>
|
|
74
83
|
*/
|
|
75
|
-
// TODO(burdon): Rename to OBJECT? (BREAKING CHANGE).
|
|
76
|
-
// 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?
|
|
77
86
|
ECHO: 'echo',
|
|
78
87
|
|
|
79
88
|
/**
|
|
@@ -85,14 +94,19 @@ export class DXN {
|
|
|
85
94
|
QUEUE: 'queue',
|
|
86
95
|
});
|
|
87
96
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
97
|
+
/**
|
|
98
|
+
* Exactly equals.
|
|
99
|
+
*/
|
|
92
100
|
static equals(a: DXN, b: DXN): boolean {
|
|
93
101
|
return a.kind === b.kind && a.parts.length === b.parts.length && a.parts.every((part, i) => part === b.parts[i]);
|
|
94
102
|
}
|
|
95
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
|
+
|
|
96
110
|
// TODO(burdon): Rename isValid.
|
|
97
111
|
static isDXNString(dxn: string): boolean {
|
|
98
112
|
return dxn.startsWith('dxn:');
|
|
@@ -119,37 +133,47 @@ export class DXN {
|
|
|
119
133
|
static tryParse(dxn: string): DXN | undefined {
|
|
120
134
|
try {
|
|
121
135
|
return DXN.parse(dxn);
|
|
122
|
-
} catch
|
|
136
|
+
} catch {
|
|
123
137
|
return undefined;
|
|
124
138
|
}
|
|
125
139
|
}
|
|
126
140
|
|
|
127
141
|
/**
|
|
128
|
-
* @example `dxn:type:example.
|
|
142
|
+
* @example `dxn:type:com.example.type.person`
|
|
129
143
|
*/
|
|
130
144
|
static fromTypename(typename: string): DXN {
|
|
131
145
|
return new DXN(DXN.kind.TYPE, [typename]);
|
|
132
146
|
}
|
|
133
147
|
|
|
134
148
|
/**
|
|
135
|
-
* @example `dxn:type:example.
|
|
149
|
+
* @example `dxn:type:com.example.type.person:0.1.0`
|
|
136
150
|
*/
|
|
137
151
|
// TODO(dmaretskyi): Consider using @ as the version separator.
|
|
138
152
|
static fromTypenameAndVersion(typename: string, version: string): DXN {
|
|
139
153
|
return new DXN(DXN.kind.TYPE, [typename, version]);
|
|
140
154
|
}
|
|
141
155
|
|
|
156
|
+
/**
|
|
157
|
+
* @example `dxn:echo:BA25QRC2FEWCSAMRP4RZL65LWJ7352CKE:01J00J9B45YHYSGZQTQMSKMGJ6`
|
|
158
|
+
*/
|
|
159
|
+
static fromSpaceAndObjectId(spaceId: SpaceId, objectId: ObjectId): DXN {
|
|
160
|
+
assertArgument(SpaceId.isValid(spaceId), `Invalid space ID: ${spaceId}`);
|
|
161
|
+
assertArgument(ObjectId.isValid(objectId), 'objectId', `Invalid object ID: ${objectId}`);
|
|
162
|
+
return new DXN(DXN.kind.ECHO, [spaceId, objectId]);
|
|
163
|
+
}
|
|
164
|
+
|
|
142
165
|
/**
|
|
143
166
|
* @example `dxn:echo:@:01J00J9B45YHYSGZQTQMSKMGJ6`
|
|
144
167
|
*/
|
|
145
168
|
static fromLocalObjectId(id: string): DXN {
|
|
169
|
+
assertArgument(ObjectId.isValid(id), 'id', `Invalid object ID: ${id}`);
|
|
146
170
|
return new DXN(DXN.kind.ECHO, [LOCAL_SPACE_TAG, id]);
|
|
147
171
|
}
|
|
148
172
|
|
|
149
173
|
static fromQueue(subspaceTag: QueueSubspaceTag, spaceId: SpaceId, queueId: ObjectId, objectId?: ObjectId) {
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
174
|
+
assertArgument(SpaceId.isValid(spaceId), `Invalid space ID: ${spaceId}`);
|
|
175
|
+
assertArgument(ObjectId.isValid(queueId), 'queueId', `Invalid queue ID: ${queueId}`);
|
|
176
|
+
assertArgument(!objectId || ObjectId.isValid(objectId), 'objectId', `Invalid object ID: ${objectId}`);
|
|
153
177
|
|
|
154
178
|
return new DXN(DXN.kind.QUEUE, [subspaceTag, spaceId, queueId, ...(objectId ? [objectId] : [])]);
|
|
155
179
|
}
|
|
@@ -158,19 +182,23 @@ export class DXN {
|
|
|
158
182
|
#parts: string[];
|
|
159
183
|
|
|
160
184
|
constructor(kind: string, parts: string[]) {
|
|
161
|
-
|
|
162
|
-
|
|
185
|
+
assertArgument(parts.length > 0, 'parts', `Invalid DXN: ${parts}`);
|
|
186
|
+
assertArgument(
|
|
187
|
+
parts.every((part) => typeof part === 'string' && part.length > 0 && part.indexOf(':') === -1),
|
|
188
|
+
'parts',
|
|
189
|
+
`Invalid DXN: ${parts}`,
|
|
190
|
+
);
|
|
163
191
|
|
|
164
192
|
// Per-type validation.
|
|
165
193
|
switch (kind) {
|
|
166
194
|
case DXN.kind.TYPE:
|
|
167
195
|
if (parts.length > 2) {
|
|
168
|
-
throw new Error('Invalid
|
|
196
|
+
throw new Error('Invalid DXN.kind.TYPE');
|
|
169
197
|
}
|
|
170
198
|
break;
|
|
171
199
|
case DXN.kind.ECHO:
|
|
172
200
|
if (parts.length !== 2) {
|
|
173
|
-
throw new Error('Invalid
|
|
201
|
+
throw new Error('Invalid DXN.kind.ECHO');
|
|
174
202
|
}
|
|
175
203
|
break;
|
|
176
204
|
}
|
|
@@ -208,6 +236,10 @@ export class DXN {
|
|
|
208
236
|
};
|
|
209
237
|
}
|
|
210
238
|
|
|
239
|
+
get kind() {
|
|
240
|
+
return this.#kind;
|
|
241
|
+
}
|
|
242
|
+
|
|
211
243
|
get parts() {
|
|
212
244
|
return this.#parts;
|
|
213
245
|
}
|
|
@@ -218,6 +250,10 @@ export class DXN {
|
|
|
218
250
|
return this.#parts[0];
|
|
219
251
|
}
|
|
220
252
|
|
|
253
|
+
equals(other: DXN): boolean {
|
|
254
|
+
return DXN.equals(this, other);
|
|
255
|
+
}
|
|
256
|
+
|
|
221
257
|
hasTypenameOf(typename: string): boolean {
|
|
222
258
|
return this.#kind === DXN.kind.TYPE && this.#parts.length === 1 && this.#parts[0] === typename;
|
|
223
259
|
}
|
|
@@ -233,6 +269,7 @@ export class DXN {
|
|
|
233
269
|
|
|
234
270
|
const [type, version] = this.#parts;
|
|
235
271
|
return {
|
|
272
|
+
// TODO(wittjosiah): Should be `typename` for consistency.
|
|
236
273
|
type,
|
|
237
274
|
version: version as string | undefined,
|
|
238
275
|
};
|
|
@@ -246,6 +283,7 @@ export class DXN {
|
|
|
246
283
|
const [spaceId, echoId] = this.#parts;
|
|
247
284
|
return {
|
|
248
285
|
spaceId: spaceId === LOCAL_SPACE_TAG ? undefined : (spaceId as SpaceId | undefined),
|
|
286
|
+
// TODO(burdon): objectId.
|
|
249
287
|
echoId,
|
|
250
288
|
};
|
|
251
289
|
}
|
|
@@ -261,41 +299,33 @@ export class DXN {
|
|
|
261
299
|
}
|
|
262
300
|
|
|
263
301
|
return {
|
|
264
|
-
subspaceTag,
|
|
302
|
+
subspaceTag: subspaceTag as QueueSubspaceTag,
|
|
265
303
|
spaceId: spaceId as SpaceId,
|
|
266
304
|
queueId,
|
|
267
305
|
objectId: objectId as string | undefined,
|
|
268
306
|
};
|
|
269
307
|
}
|
|
270
|
-
}
|
|
271
|
-
|
|
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
308
|
|
|
290
|
-
|
|
291
|
-
|
|
309
|
+
/**
|
|
310
|
+
* Produces a new DXN with the given parts appended.
|
|
311
|
+
*/
|
|
312
|
+
extend(parts: string[]): DXN {
|
|
313
|
+
return new DXN(this.#kind, [...this.#parts, ...parts]);
|
|
314
|
+
}
|
|
292
315
|
}
|
|
293
|
-
```
|
|
294
316
|
|
|
295
317
|
/**
|
|
296
318
|
* API namespace.
|
|
297
319
|
*/
|
|
298
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
|
+
|
|
299
329
|
export type TypeDXN = {
|
|
300
330
|
type: string;
|
|
301
331
|
version?: string;
|
|
@@ -303,22 +333,13 @@ export declare namespace DXN {
|
|
|
303
333
|
|
|
304
334
|
export type EchoDXN = {
|
|
305
335
|
spaceId?: SpaceId;
|
|
306
|
-
// TODO(
|
|
307
|
-
echoId: string; // TODO(dmaretskyi): ObjectId.
|
|
336
|
+
echoId: string; // TODO(dmaretskyi): Rename to `objectId` and use `ObjectId` for the type.
|
|
308
337
|
};
|
|
309
338
|
|
|
310
339
|
export type QueueDXN = {
|
|
311
|
-
subspaceTag:
|
|
340
|
+
subspaceTag: QueueSubspaceTag;
|
|
312
341
|
spaceId: SpaceId;
|
|
313
342
|
queueId: string; // TODO(dmaretskyi): ObjectId.
|
|
314
343
|
objectId?: string; // TODO(dmaretskyi): ObjectId.
|
|
315
344
|
};
|
|
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
345
|
}
|
package/src/index.ts
CHANGED
package/src/object-id.ts
CHANGED
|
@@ -2,23 +2,71 @@
|
|
|
2
2
|
// Copyright 2025 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import
|
|
6
|
-
import {
|
|
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: '
|
|
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;
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* WARNING: To be used only within tests.
|
|
55
|
+
*
|
|
56
|
+
* Pins the time component of generated ObjectIds and seeds the PRNG used for the random component,
|
|
57
|
+
* causing the same sequence of IDs to be generated across runs.
|
|
58
|
+
* Do not use in production code as this will cause data collisions.
|
|
59
|
+
*
|
|
60
|
+
* @param time - Fixed timestamp (ms since epoch) used as the ULID time component.
|
|
61
|
+
* @param seed - Seed value for the PRNG used for the ULID random component.
|
|
62
|
+
*
|
|
63
|
+
* ```ts
|
|
64
|
+
* ObjectId.dangerouslySetSeed(new Date('2025-01-01').getTime(), 42);
|
|
65
|
+
* ```
|
|
66
|
+
*
|
|
67
|
+
* NOTE: The generated IDs depend on the order of ObjectId.random() calls, which might be affected by test order, scheduling, etc.
|
|
68
|
+
*/
|
|
69
|
+
dangerouslySetSeed(time: number, seed: number): void;
|
|
22
70
|
}
|
|
23
71
|
|
|
24
72
|
/**
|
|
@@ -27,16 +75,80 @@ export interface ObjectIdClass extends Schema.SchemaClass<ObjectId, string> {
|
|
|
27
75
|
* Follows ULID spec.
|
|
28
76
|
*/
|
|
29
77
|
export const ObjectId: ObjectIdClass = class extends ObjectIdSchema {
|
|
78
|
+
static #factory: ULIDFactory = monotonicFactory();
|
|
79
|
+
static #seedTime: number | undefined = undefined;
|
|
80
|
+
|
|
30
81
|
static isValid(id: string): id is ObjectId {
|
|
31
82
|
try {
|
|
32
83
|
Schema.decodeSync(ObjectId)(id);
|
|
33
84
|
return true;
|
|
34
|
-
} catch
|
|
85
|
+
} catch {
|
|
35
86
|
return false;
|
|
36
87
|
}
|
|
37
88
|
}
|
|
38
89
|
|
|
39
90
|
static random(): ObjectId {
|
|
40
|
-
return
|
|
91
|
+
return this.#factory(this.#seedTime) as ObjectId;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
static dangerouslyDisableRandomness() {
|
|
95
|
+
this.#factory = monotonicFactory(makeTestPRNG());
|
|
96
|
+
this.#seedTime = new Date('2025-01-01').getTime();
|
|
41
97
|
}
|
|
98
|
+
|
|
99
|
+
static dangerouslySetSeed(time: number, seed: number) {
|
|
100
|
+
this.#factory = monotonicFactory(makeTestPRNG(seed));
|
|
101
|
+
this.#seedTime = time;
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Test PRNG that always starts with the same seed and produces the same sequence.
|
|
107
|
+
*/
|
|
108
|
+
const makeTestPRNG = (seed: number = 0): PRNG => {
|
|
109
|
+
const rng = new SimplePRNG(seed);
|
|
110
|
+
return () => {
|
|
111
|
+
return rng.next();
|
|
112
|
+
};
|
|
42
113
|
};
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Simple Linear Congruential Generator (LCG) for pseudo-random number generation.
|
|
117
|
+
* Returns numbers in the range [0, 1) (0 inclusive, 1 exclusive).
|
|
118
|
+
*/
|
|
119
|
+
export class SimplePRNG {
|
|
120
|
+
#seed: number;
|
|
121
|
+
|
|
122
|
+
// LCG parameters (from Numerical Recipes)
|
|
123
|
+
static readonly #a = 1664525;
|
|
124
|
+
static readonly #c = 1013904223;
|
|
125
|
+
static readonly #m = Math.pow(2, 32);
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Creates a new PRNG instance.
|
|
129
|
+
* @param seed - Initial seed value. If not provided, uses 0.
|
|
130
|
+
*/
|
|
131
|
+
constructor(seed: number = 0) {
|
|
132
|
+
this.#seed = seed;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Generates the next pseudo-random number in the range [0, 1).
|
|
137
|
+
* @returns A pseudo-random number between 0 (inclusive) and 1 (exclusive).
|
|
138
|
+
*/
|
|
139
|
+
next(): number {
|
|
140
|
+
// Update seed using LCG formula: (a * seed + c) mod m
|
|
141
|
+
this.#seed = (SimplePRNG.#a * this.#seed + SimplePRNG.#c) % SimplePRNG.#m;
|
|
142
|
+
|
|
143
|
+
// Normalize to [0, 1) range
|
|
144
|
+
return this.#seed / SimplePRNG.#m;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Resets the generator with a new seed.
|
|
149
|
+
* @param seed - New seed value.
|
|
150
|
+
*/
|
|
151
|
+
reset(seed: number): void {
|
|
152
|
+
this.#seed = seed;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2024 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { describe, expect, test } from 'vitest';
|
|
6
|
+
|
|
7
|
+
import { parseId } from './parse-id';
|
|
8
|
+
|
|
9
|
+
describe('parseId', () => {
|
|
10
|
+
test('space id', () => {
|
|
11
|
+
const id = '123456789012345678901234567890123';
|
|
12
|
+
expect(parseId(id)).toEqual({ spaceId: id });
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
test('object id', () => {
|
|
16
|
+
const id = '12345678901234567890123456';
|
|
17
|
+
expect(parseId(id)).toEqual({ objectId: id });
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
test('fully qualified id', () => {
|
|
21
|
+
const id = '123456789012345678901234567890123:12345678901234567890123456';
|
|
22
|
+
expect(parseId(id)).toEqual({
|
|
23
|
+
spaceId: '123456789012345678901234567890123',
|
|
24
|
+
objectId: '12345678901234567890123456',
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
test('invalid id', () => {
|
|
29
|
+
const id = '123456789012345678901234561234567890123456';
|
|
30
|
+
expect(parseId(id)).toEqual({});
|
|
31
|
+
});
|
|
32
|
+
});
|
package/src/parse-id.ts
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2024 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { type ObjectId } from './object-id';
|
|
6
|
+
import { type SpaceId } from './space-id';
|
|
7
|
+
|
|
8
|
+
export const SPACE_ID_LENGTH = 33;
|
|
9
|
+
export const OBJECT_ID_LENGTH = 26;
|
|
10
|
+
export const FQ_ID_LENGTH = SPACE_ID_LENGTH + OBJECT_ID_LENGTH + 1;
|
|
11
|
+
|
|
12
|
+
export const parseId = (id?: string): { spaceId?: SpaceId; objectId?: ObjectId } => {
|
|
13
|
+
if (!id) {
|
|
14
|
+
return {};
|
|
15
|
+
} else if (id.length === SPACE_ID_LENGTH) {
|
|
16
|
+
return {
|
|
17
|
+
spaceId: id as SpaceId,
|
|
18
|
+
};
|
|
19
|
+
} else if (id.length === OBJECT_ID_LENGTH) {
|
|
20
|
+
return {
|
|
21
|
+
objectId: id as ObjectId,
|
|
22
|
+
};
|
|
23
|
+
} else if (id.length === FQ_ID_LENGTH && id.indexOf(':') === SPACE_ID_LENGTH) {
|
|
24
|
+
const [spaceId, objectId] = id.split(':');
|
|
25
|
+
return {
|
|
26
|
+
spaceId: spaceId as SpaceId,
|
|
27
|
+
objectId: objectId as ObjectId,
|
|
28
|
+
};
|
|
29
|
+
} else {
|
|
30
|
+
return {};
|
|
31
|
+
}
|
|
32
|
+
};
|
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
|
@@ -4,13 +4,13 @@
|
|
|
4
4
|
|
|
5
5
|
import base32Decode from 'base32-decode';
|
|
6
6
|
import base32Encode from 'base32-encode';
|
|
7
|
-
import { type
|
|
7
|
+
import { type InspectOptionsStylized, type inspect } from 'node:util';
|
|
8
8
|
|
|
9
9
|
import {
|
|
10
|
-
devtoolsFormatter,
|
|
11
10
|
type DevtoolsFormatter,
|
|
12
|
-
equalsSymbol,
|
|
13
11
|
type Equatable,
|
|
12
|
+
devtoolsFormatter,
|
|
13
|
+
equalsSymbol,
|
|
14
14
|
inspectCustom,
|
|
15
15
|
truncateKey,
|
|
16
16
|
} from '@dxos/debug';
|
|
@@ -216,7 +216,7 @@ export class PublicKey implements Equatable {
|
|
|
216
216
|
return 'B' + base32Encode(this._value, 'RFC4648');
|
|
217
217
|
}
|
|
218
218
|
|
|
219
|
-
truncate(length
|
|
219
|
+
truncate(length?: number): string {
|
|
220
220
|
return truncateKey(this, length);
|
|
221
221
|
}
|
|
222
222
|
|
package/src/random-bytes.ts
CHANGED
|
@@ -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-
|
|
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);
|