@dxos/keys 0.5.8 → 0.5.9-main.079a532

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.5.8",
3
+ "version": "0.5.9-main.079a532",
4
4
  "description": "Key utils and definitions.",
5
5
  "homepage": "https://dxos.org",
6
6
  "bugs": "https://github.com/dxos/dxos/issues",
@@ -21,9 +21,13 @@
21
21
  "src"
22
22
  ],
23
23
  "dependencies": {
24
- "@dxos/debug": "0.5.8",
25
- "@dxos/invariant": "0.5.8",
26
- "@dxos/node-std": "0.5.8"
24
+ "@dxos/debug": "0.5.9-main.079a532",
25
+ "@dxos/invariant": "0.5.9-main.079a532",
26
+ "@dxos/node-std": "0.5.9-main.079a532"
27
+ },
28
+ "devDependencies": {
29
+ "base32-decode": "^1.0.0",
30
+ "base32-encode": "^2.0.0"
27
31
  },
28
32
  "publishConfig": {
29
33
  "access": "public"
package/src/dxn.ts ADDED
@@ -0,0 +1,88 @@
1
+ //
2
+ // Copyright 2024 DXOS.org
3
+ //
4
+
5
+ import { invariant } from '@dxos/invariant';
6
+
7
+ /**
8
+ * DXN unambiguously names a resource like an ECHO object, schema definition, plugin, etc.
9
+ * Each DXN starts with a dx prefix, followed by a resource kind.
10
+ * Colon Symbol : is used a delimiter between parts.
11
+ * DXNs may contain slashes.
12
+ * '@' in the place of the space id is used to denote that the DXN should be resolved in the local space.
13
+ *
14
+ * @example
15
+ *
16
+ * ```
17
+ * dx:echo:<space key>:<echo id>
18
+ * dx:echo:BA25QRC2FEWCSAMRP4RZL65LWJ7352CKE:01J00J9B45YHYSGZQTQMSKMGJ6
19
+ * dx:echo:@:01J00J9B45YHYSGZQTQMSKMGJ6
20
+ * dx:type:dxos.org/type/Calendar
21
+ * dx:plugin:dxos.org/agent/plugin/functions
22
+ * ```
23
+ */
24
+ export class DXN {
25
+ /**
26
+ * Kind constants.
27
+ */
28
+ static kind = Object.freeze({
29
+ ECHO: 'echo',
30
+ TYPE: 'type',
31
+ });
32
+
33
+ static parse(dxn: string): DXN {
34
+ const [prefix, kind, ...parts] = dxn.split(':');
35
+ if (!(prefix === 'dxn')) {
36
+ throw new Error('Invalid DXN');
37
+ }
38
+ if (!(typeof kind === 'string' && kind.length > 0)) {
39
+ throw new Error('Invalid DXN');
40
+ }
41
+ if (!(parts.length > 0)) {
42
+ throw new Error('Invalid DXN');
43
+ }
44
+ return new DXN(kind, parts);
45
+ }
46
+
47
+ #kind: string;
48
+ #parts: string[];
49
+
50
+ constructor(kind: string, parts: string[]) {
51
+ invariant(parts.length > 0);
52
+ invariant(parts.every((part) => typeof part === 'string' && part.length > 0 && part.indexOf(':') === -1));
53
+
54
+ // Per-type validation.
55
+ switch (kind) {
56
+ case DXN.kind.ECHO:
57
+ invariant(parts.length === 2);
58
+ break;
59
+ case DXN.kind.TYPE:
60
+ invariant(parts.length === 1);
61
+ break;
62
+ }
63
+
64
+ this.#kind = kind;
65
+ this.#parts = parts;
66
+ }
67
+
68
+ get kind() {
69
+ return this.#kind;
70
+ }
71
+
72
+ get parts() {
73
+ return this.#parts;
74
+ }
75
+
76
+ isTypeDXNOf(typename: string) {
77
+ return this.#kind === DXN.kind.TYPE && this.#parts.length === 1 && this.#parts[0] === typename;
78
+ }
79
+
80
+ toString() {
81
+ return `dxn:${this.#kind}:${this.#parts.join(':')}`;
82
+ }
83
+ }
84
+
85
+ /**
86
+ * Tags for ECHO DXNs that should resolve the object ID in the local space.
87
+ */
88
+ export const LOCAL_SPACE_TAG = '@';
package/src/index.ts CHANGED
@@ -4,3 +4,5 @@
4
4
 
5
5
  export * from './public-key';
6
6
  export * from './types';
7
+ export * from './space-id';
8
+ export * from './dxn';
@@ -60,4 +60,11 @@ describe('PublicKey', () => {
60
60
  const key = PublicKey.random();
61
61
  expect(PublicKey.equals(key, PublicKey.from(key.toHex()))).to.be.true;
62
62
  });
63
+
64
+ test('base32', () => {
65
+ const key = PublicKey.randomOfLength(20); // Space keys will be cut to first 20 bytes of sha-256 hash.
66
+ const encoded = key.toMultibase32();
67
+
68
+ expect(PublicKey.fromMultibase32(encoded).toHex()).to.equal(key.toHex());
69
+ });
63
70
  });
package/src/public-key.ts CHANGED
@@ -2,11 +2,15 @@
2
2
  // Copyright 2020 DXOS.org
3
3
  //
4
4
 
5
+ import base32Decode from 'base32-decode';
6
+ import base32Encode from 'base32-encode';
5
7
  import { inspect, type InspectOptionsStylized } from 'node:util';
6
8
 
7
9
  import { truncateKey, devtoolsFormatter, type DevtoolsFormatter, equalsSymbol, type Equatable } from '@dxos/debug';
8
10
  import { invariant } from '@dxos/invariant';
9
11
 
12
+ import { randomBytes } from './random-bytes';
13
+
10
14
  export const PUBLIC_KEY_LENGTH = 32;
11
15
  export const SECRET_KEY_LENGTH = 64;
12
16
 
@@ -89,6 +93,10 @@ export class PublicKey implements Equatable {
89
93
  return PublicKey.from(randomBytes(PUBLIC_KEY_LENGTH));
90
94
  }
91
95
 
96
+ static randomOfLength(length: number): PublicKey {
97
+ return PublicKey.from(randomBytes(length));
98
+ }
99
+
92
100
  static *randomSequence(): Generator<PublicKey> {
93
101
  for (let i = 0; i < 1_0000; i++) {
94
102
  // Counter just to protect against infinite loops.
@@ -157,6 +165,12 @@ export class PublicKey implements Equatable {
157
165
  return key.toHex();
158
166
  }
159
167
 
168
+ static fromMultibase32(encoded: string): PublicKey {
169
+ invariant(encoded.startsWith('B'), 'Invalid multibase32 encoding');
170
+
171
+ return new PublicKey(new Uint8Array(base32Decode(encoded.slice(1), 'RFC4648')));
172
+ }
173
+
160
174
  constructor(private readonly _value: Uint8Array) {
161
175
  if (!(_value instanceof Uint8Array)) {
162
176
  throw new TypeError(`Expected Uint8Array, got: ${_value}`);
@@ -183,6 +197,10 @@ export class PublicKey implements Equatable {
183
197
  return this.asBuffer().toString('hex');
184
198
  }
185
199
 
200
+ toMultibase32(): string {
201
+ return 'B' + base32Encode(this._value, 'RFC4648');
202
+ }
203
+
186
204
  truncate(length = undefined) {
187
205
  return truncateKey(this, length);
188
206
  }
@@ -291,13 +309,3 @@ export class PublicKey implements Equatable {
291
309
  return this.equals(other);
292
310
  }
293
311
  }
294
-
295
- const randomBytes = (length: number) => {
296
- // globalThis.crypto is not available in Node.js when running in vitest even though the documentation says it should be.
297
- // eslint-disable-next-line @typescript-eslint/no-var-requires
298
- const webCrypto = globalThis.crypto ?? require('node:crypto').webcrypto;
299
-
300
- const bytes = new Uint8Array(length);
301
- webCrypto.getRandomValues(bytes);
302
- return bytes;
303
- };
@@ -0,0 +1,13 @@
1
+ //
2
+ // Copyright 2024 DXOS.org
3
+ //
4
+
5
+ export const randomBytes = (length: number) => {
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
8
+ const webCrypto = globalThis.crypto ?? require('node:crypto').webcrypto;
9
+
10
+ const bytes = new Uint8Array(length);
11
+ webCrypto.getRandomValues(bytes);
12
+ return bytes;
13
+ };
@@ -0,0 +1,17 @@
1
+ //
2
+ // Copyright 2024 DXOS.org
3
+ //
4
+
5
+ import { expect, test } from 'vitest';
6
+
7
+ import { SpaceId } from './space-id';
8
+
9
+ test('space-id', () => {
10
+ const id = SpaceId.random();
11
+
12
+ expect(SpaceId.isValid(id)).toBe(true);
13
+ expect(id.length).toBe(33);
14
+ const decoded = SpaceId.decode(id);
15
+ expect(decoded.length).toBe(SpaceId.byteLength);
16
+ expect(SpaceId.encode(decoded)).toBe(id);
17
+ });
@@ -0,0 +1,45 @@
1
+ //
2
+ // Copyright 2024 DXOS.org
3
+ //
4
+
5
+ import base32Decode from 'base32-decode';
6
+ import base32Encode from 'base32-encode';
7
+
8
+ import { invariant } from '@dxos/invariant';
9
+
10
+ import { randomBytes } from './random-bytes';
11
+
12
+ /**
13
+ * A unique identifier for a space.
14
+ * Space keys are generated by creating a keypair, and then taking the first 20 bytes of the SHA-256 hash of the public key and encoding them to multibase RFC4648 base-32 format (prefixed with B, see Multibase Table).
15
+ * @example BA25QRC2FEWCSAMRP4RZL65LWJ7352CKE
16
+ */
17
+ export type SpaceId = string & { __SpaceId: true };
18
+
19
+ export const SpaceId = Object.freeze({
20
+ byteLength: 20,
21
+ encode: (value: Uint8Array): SpaceId => {
22
+ invariant(value instanceof Uint8Array, 'Invalid type');
23
+ invariant(value.length === SpaceId.byteLength, 'Invalid length');
24
+
25
+ return (MULTIBASE_PREFIX + base32Encode(value, 'RFC4648')) as SpaceId;
26
+ },
27
+ decode: (value: SpaceId): Uint8Array => {
28
+ invariant(value.startsWith(MULTIBASE_PREFIX), 'Invalid multibase32 encoding');
29
+
30
+ return new Uint8Array(base32Decode(value.slice(1), 'RFC4648'));
31
+ },
32
+ isValid: (value: string): value is SpaceId => {
33
+ return typeof value === 'string' && value.startsWith(MULTIBASE_PREFIX) && value.length === ENCODED_LENGTH;
34
+ },
35
+ random: (): SpaceId => {
36
+ return SpaceId.encode(randomBytes(SpaceId.byteLength));
37
+ },
38
+ });
39
+
40
+ /**
41
+ * Denotes RFC4648 base-32 format.
42
+ */
43
+ const MULTIBASE_PREFIX = 'B';
44
+
45
+ const ENCODED_LENGTH = 33;
package/src/types.ts CHANGED
@@ -2,7 +2,6 @@
2
2
  // Copyright 2022 DXOS.org
3
3
  //
4
4
 
5
- // TODO(burdon): Replace with hypercore-crypto type.
6
5
  export type KeyPair = {
7
6
  publicKey: Buffer;
8
7
  secretKey: Buffer;