@dxos/keys 0.10.0 → 0.11.1
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/dist/lib/browser/index.mjs +17 -6
- package/dist/lib/browser/index.mjs.map +3 -3
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/node-esm/index.mjs +10 -4
- package/dist/lib/node-esm/index.mjs.map +3 -3
- package/dist/lib/node-esm/meta.json +1 -1
- package/dist/types/src/DXN.d.ts +19 -8
- package/dist/types/src/DXN.d.ts.map +1 -1
- package/dist/types/src/EID.d.ts +17 -3
- package/dist/types/src/EID.d.ts.map +1 -1
- package/dist/types/src/keys.workerd.test.d.ts +2 -0
- package/dist/types/src/keys.workerd.test.d.ts.map +1 -0
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/package.json +4 -4
- package/src/DXN.test.ts +3 -0
- package/src/DXN.ts +34 -12
- package/src/EID.test.ts +11 -2
- package/src/EID.ts +27 -9
- package/src/keys.workerd.test.ts +47 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dxos/keys",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.1",
|
|
4
4
|
"description": "Key utils and definitions.",
|
|
5
5
|
"homepage": "https://dxos.org",
|
|
6
6
|
"bugs": "https://github.com/dxos/dxos/issues",
|
|
@@ -27,9 +27,9 @@
|
|
|
27
27
|
],
|
|
28
28
|
"dependencies": {
|
|
29
29
|
"ulidx": "^2.3.0",
|
|
30
|
-
"@dxos/debug": "0.
|
|
31
|
-
"@dxos/
|
|
32
|
-
"@dxos/
|
|
30
|
+
"@dxos/debug": "0.11.1",
|
|
31
|
+
"@dxos/invariant": "0.11.1",
|
|
32
|
+
"@dxos/node-std": "0.11.1"
|
|
33
33
|
},
|
|
34
34
|
"devDependencies": {
|
|
35
35
|
"base32-decode": "^1.0.0",
|
package/src/DXN.test.ts
CHANGED
|
@@ -45,6 +45,9 @@ describe('DXN.Name', () => {
|
|
|
45
45
|
// No dots — single segment.
|
|
46
46
|
// @ts-expect-error
|
|
47
47
|
DXN.make('unknown');
|
|
48
|
+
// One dot — two segments, below the three-segment minimum.
|
|
49
|
+
// @ts-expect-error
|
|
50
|
+
DXN.make('a.b');
|
|
48
51
|
// Hyphen in the FINAL segment.
|
|
49
52
|
// @ts-expect-error
|
|
50
53
|
DXN.make('com.example.type.registry-entry');
|
package/src/DXN.ts
CHANGED
|
@@ -31,30 +31,52 @@ const DXN_SPEC_REGEXP =
|
|
|
31
31
|
export type DXN = URI.URI & { readonly __DXN: unique symbol };
|
|
32
32
|
|
|
33
33
|
/**
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
* Checks two rules expressible with template literal types:
|
|
37
|
-
* - Must contain at least one dot (multi-segment).
|
|
38
|
-
* - Final segment (after the last dot) must not contain a hyphen.
|
|
34
|
+
* Recursive segment-chain check used by {@link Name}: hyphens are permitted in
|
|
35
|
+
* every segment except the truly final one.
|
|
39
36
|
*
|
|
40
37
|
* TypeScript template literal inference is non-greedy: `${string}.${infer Rest}`
|
|
41
38
|
* always splits at the first dot. The type recurses until `Rest` has no more dots,
|
|
42
39
|
* at which point it is the true final segment and is checked for hyphens.
|
|
40
|
+
*/
|
|
41
|
+
type ValidSegmentChain<T extends string> = T extends `${string}.${infer Rest}`
|
|
42
|
+
? Rest extends `${string}.${string}`
|
|
43
|
+
? [ValidSegmentChain<Rest>] extends [never]
|
|
44
|
+
? never
|
|
45
|
+
: T
|
|
46
|
+
: Rest extends `${string}-${string}`
|
|
47
|
+
? never
|
|
48
|
+
: T
|
|
49
|
+
: never;
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Compile-time validation for NSID strings (the `dxn:` prefix is absent here).
|
|
53
|
+
*
|
|
54
|
+
* Checks two rules expressible with template literal types:
|
|
55
|
+
* - Three-segment minimum (at least two dots) for names that are fully known at
|
|
56
|
+
* compile time — matches the runtime grammar in {@link DXN_SPEC_REGEXP} and
|
|
57
|
+
* `parse`.
|
|
58
|
+
* - Final segment (after the last dot) must not contain a hyphen.
|
|
43
59
|
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
60
|
+
* The three-segment minimum only applies once `Head` (the portion before the
|
|
61
|
+
* first dot) resolves to a concrete literal, so a fully literal two-segment
|
|
62
|
+
* name like `a.b` is rejected. Template-literal call sites whose prefix is a
|
|
63
|
+
* runtime `string` (e.g. `` `${meta.key}.event` ``) can't be proven to have
|
|
64
|
+
* enough segments at compile time — `Head` there infers as `string` itself —
|
|
65
|
+
* so only the known final segment is checked; the rest is validated at
|
|
46
66
|
* runtime by the regex inside `parse`.
|
|
47
67
|
*/
|
|
48
68
|
export type Name<T extends string> = [string] extends [T]
|
|
49
69
|
? string
|
|
50
|
-
: T extends `${
|
|
70
|
+
: T extends `${infer Head}.${infer Rest}`
|
|
51
71
|
? Rest extends `${string}.${string}`
|
|
52
|
-
? [
|
|
53
|
-
? never
|
|
54
|
-
: T
|
|
55
|
-
: Rest extends `${string}-${string}`
|
|
72
|
+
? [ValidSegmentChain<Rest>] extends [never]
|
|
56
73
|
? never
|
|
57
74
|
: T
|
|
75
|
+
: [string] extends [Head]
|
|
76
|
+
? Rest extends `${string}-${string}`
|
|
77
|
+
? never
|
|
78
|
+
: T
|
|
79
|
+
: never
|
|
58
80
|
: never;
|
|
59
81
|
|
|
60
82
|
/**
|
package/src/EID.test.ts
CHANGED
|
@@ -18,7 +18,7 @@ describe('EID.make', () => {
|
|
|
18
18
|
});
|
|
19
19
|
|
|
20
20
|
test('produces local echo URI from objectId only', ({ expect }) => {
|
|
21
|
-
expect(EID.make({ entityId: OBJECT })).toBe(`echo
|
|
21
|
+
expect(EID.make({ entityId: OBJECT })).toBe(`echo:///${OBJECT}`);
|
|
22
22
|
});
|
|
23
23
|
|
|
24
24
|
test('produces space-only echo URI from spaceId only', ({ expect }) => {
|
|
@@ -52,6 +52,11 @@ describe('EID.parse', () => {
|
|
|
52
52
|
test('passes through canonical format unchanged', ({ expect }) => {
|
|
53
53
|
const id = `echo://${SPACE}/${OBJECT}`;
|
|
54
54
|
expect(EID.parse(id)).toBe(id);
|
|
55
|
+
expect(EID.parse(`echo:///${OBJECT}`)).toBe(`echo:///${OBJECT}`);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test('normalizes the legacy single-slash local form to triple-slash', ({ expect }) => {
|
|
59
|
+
expect(EID.parse(`echo:/${OBJECT}`)).toBe(`echo:///${OBJECT}`);
|
|
55
60
|
});
|
|
56
61
|
|
|
57
62
|
test('throws on invalid input', ({ expect }) => {
|
|
@@ -67,7 +72,7 @@ describe('EID.parse', () => {
|
|
|
67
72
|
describe('EID.tryParse', () => {
|
|
68
73
|
test('returns undefined on failure instead of throwing', ({ expect }) => {
|
|
69
74
|
expect(EID.tryParse('not-a-uri')).toBeUndefined();
|
|
70
|
-
expect(EID.tryParse(`echo:/${OBJECT}`)).toBe(`echo
|
|
75
|
+
expect(EID.tryParse(`echo:/${OBJECT}`)).toBe(`echo:///${OBJECT}`);
|
|
71
76
|
});
|
|
72
77
|
});
|
|
73
78
|
|
|
@@ -124,6 +129,10 @@ describe('EID.equals', () => {
|
|
|
124
129
|
const b = EID.make({ spaceId: SPACE, entityId: OBJECT2 });
|
|
125
130
|
expect(EID.equals(a, b)).toBe(false);
|
|
126
131
|
});
|
|
132
|
+
|
|
133
|
+
test('treats the legacy single-slash and canonical triple-slash local forms as equal', ({ expect }) => {
|
|
134
|
+
expect(EID.equals(`echo:/${OBJECT}` as EID.EID, `echo:///${OBJECT}` as EID.EID)).toBe(true);
|
|
135
|
+
});
|
|
127
136
|
});
|
|
128
137
|
|
|
129
138
|
describe('EID.toLocal', () => {
|
package/src/EID.ts
CHANGED
|
@@ -10,25 +10,38 @@ import type { EntityId } from './entity-id';
|
|
|
10
10
|
import type { SpaceId } from './space-id';
|
|
11
11
|
import type * as URI from './URI';
|
|
12
12
|
|
|
13
|
-
// Canonical-form regex covering all
|
|
13
|
+
// Canonical-form regex covering all accepted EID shapes.
|
|
14
14
|
// echo://<spaceId>/<objectId>
|
|
15
15
|
// echo://<spaceId>
|
|
16
|
-
// echo
|
|
17
|
-
// echo
|
|
16
|
+
// echo:///<objectId> (local, canonical)
|
|
17
|
+
// echo:/<objectId> (local, legacy — accepted on read, normalized to the canonical triple-slash form)
|
|
18
18
|
const ECHO_URI_REGEXP = /^echo:(?:\/\/[^/]+(?:\/[^/]+)?|(?:\/\/\/|\/)[^/]+)$/;
|
|
19
19
|
|
|
20
20
|
// Sub-patterns used for extraction.
|
|
21
21
|
const QUALIFIED_RE = /^echo:\/\/([^/]+)\/([^/]+)$/;
|
|
22
22
|
const SPACE_ONLY_RE = /^echo:\/\/([^/]+)$/;
|
|
23
23
|
const LOCAL_RE = /^echo:(?:\/\/\/|\/)([^/]+)$/;
|
|
24
|
+
// Legacy single-slash local form (`echo:/<objectId>`), distinguished from the canonical
|
|
25
|
+
// triple-slash form so `parse` can normalize it. The negative lookahead rejects `echo://…`.
|
|
26
|
+
const LOCAL_LEGACY_RE = /^echo:\/(?!\/)([^/]+)$/;
|
|
24
27
|
|
|
25
28
|
/**
|
|
26
29
|
* Addresses an ECHO object or space. Uses the `echo:` URI scheme.
|
|
27
30
|
*
|
|
31
|
+
* Canonical forms:
|
|
32
|
+
* - `echo://<spaceId>/<objectId>` — fully-qualified object.
|
|
33
|
+
* - `echo://<spaceId>` — space.
|
|
34
|
+
* - `echo:///<objectId>` — local (space-less) object.
|
|
35
|
+
*
|
|
36
|
+
* @deprecated form: the single-slash local form `echo:/<objectId>` is retired in favour of the
|
|
37
|
+
* triple-slash `echo:///<objectId>` form. It is still accepted on read (and normalized by `parse`)
|
|
38
|
+
* so existing persisted data keeps resolving, but it is no longer produced — do not emit it in new
|
|
39
|
+
* code. Construct local EIDs with `make({ entityId })`.
|
|
40
|
+
*
|
|
28
41
|
* @example
|
|
29
42
|
* ```
|
|
30
43
|
* echo://BA25QRC2FEWCSAMRP4RZL65LWJ7352CKE/01J00J9B45YHYSGZQTQMSKMGJ6
|
|
31
|
-
* echo
|
|
44
|
+
* echo:///01J00J9B45YHYSGZQTQMSKMGJ6
|
|
32
45
|
* echo://BA25QRC2FEWCSAMRP4RZL65LWJ7352CKE
|
|
33
46
|
* ```
|
|
34
47
|
*/
|
|
@@ -40,13 +53,18 @@ export type EID = URI.URI & { readonly __EID: unique symbol };
|
|
|
40
53
|
export const isEID = (value: unknown): value is EID => typeof value === 'string' && value.startsWith('echo:');
|
|
41
54
|
|
|
42
55
|
/**
|
|
43
|
-
* Parses a string to EID. Throws if the string is not a valid
|
|
56
|
+
* Parses a string to EID. Throws if the string is not a valid `echo:` EID.
|
|
57
|
+
*
|
|
58
|
+
* The legacy single-slash local form (`echo:/<objectId>`) is accepted and normalized to the
|
|
59
|
+
* canonical triple-slash form (`echo:///<objectId>`) so that legacy and freshly-produced EIDs
|
|
60
|
+
* compare equal.
|
|
44
61
|
*/
|
|
45
62
|
export const parse = (uri: string): EID => {
|
|
46
63
|
if (!ECHO_URI_REGEXP.test(uri)) {
|
|
47
64
|
throw new Error(`Invalid EID: ${uri}`);
|
|
48
65
|
}
|
|
49
|
-
|
|
66
|
+
const legacy = LOCAL_LEGACY_RE.exec(uri);
|
|
67
|
+
return (legacy ? `echo:///${legacy[1]}` : uri) as EID;
|
|
50
68
|
};
|
|
51
69
|
|
|
52
70
|
/**
|
|
@@ -64,7 +82,7 @@ export const tryParse = (uri: string): EID | undefined => {
|
|
|
64
82
|
* Constructs an EID. Validates the result via `parse`.
|
|
65
83
|
*
|
|
66
84
|
* - `{ spaceId, entityId }` → `echo://<spaceId>/<entityId>` (fully qualified)
|
|
67
|
-
* - `{ entityId }` → `echo
|
|
85
|
+
* - `{ entityId }` → `echo:///<entityId>` (local — current space)
|
|
68
86
|
* - `{ spaceId }` → `echo://<spaceId>` (space-only)
|
|
69
87
|
*
|
|
70
88
|
* Throws if neither id is provided, or if the result is not a valid EID.
|
|
@@ -74,7 +92,7 @@ export const make = ({ spaceId, entityId }: { spaceId?: SpaceId; entityId?: Enti
|
|
|
74
92
|
if (spaceId != null && entityId != null) {
|
|
75
93
|
raw = `echo://${spaceId}/${entityId}`;
|
|
76
94
|
} else if (entityId != null) {
|
|
77
|
-
raw = `echo
|
|
95
|
+
raw = `echo:///${entityId}`;
|
|
78
96
|
} else if (spaceId != null) {
|
|
79
97
|
raw = `echo://${spaceId}`;
|
|
80
98
|
} else {
|
|
@@ -145,7 +163,7 @@ const Schema_: Schema.Schema<EID, EID> = Schema.String.pipe(
|
|
|
145
163
|
}),
|
|
146
164
|
Schema.annotations({
|
|
147
165
|
title: 'EID',
|
|
148
|
-
description: 'ECHO object/space URI: echo://<spaceId>[/<objectId>] or echo
|
|
166
|
+
description: 'ECHO object/space URI: echo://<spaceId>[/<objectId>] or echo:///<objectId>',
|
|
149
167
|
}),
|
|
150
168
|
) as unknown as Schema.Schema<EID, EID>;
|
|
151
169
|
export { Schema_ as Schema };
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2026 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
// Runs in the Cloudflare Workers runtime (`workerd`) via `@cloudflare/vitest-pool-workers`,
|
|
6
|
+
// opted in with `workerd: true` in this package's vitest config. Guards that the core key
|
|
7
|
+
// primitives keep working against the runtime our production Cloudflare functions execute on
|
|
8
|
+
// (Buffer + web-crypto `getRandomValues` under `nodejs_compat`), not just under Node.
|
|
9
|
+
|
|
10
|
+
import { describe, test } from 'vitest';
|
|
11
|
+
|
|
12
|
+
import { EntityId } from './entity-id';
|
|
13
|
+
import { PublicKey } from './public-key';
|
|
14
|
+
|
|
15
|
+
describe('keys in workerd', () => {
|
|
16
|
+
test('runs inside the Cloudflare Workers runtime', ({ expect }) => {
|
|
17
|
+
// workerd sets a fixed navigator.userAgent; asserts the pool actually swapped the runtime.
|
|
18
|
+
expect(navigator.userAgent).toBe('Cloudflare-Workers');
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
test('PublicKey.random produces distinct hex-encoded keys', ({ expect }) => {
|
|
22
|
+
const first = PublicKey.random();
|
|
23
|
+
const second = PublicKey.random();
|
|
24
|
+
expect(first.toHex()).toHaveLength(64);
|
|
25
|
+
expect(PublicKey.equals(first, first)).toBe(true);
|
|
26
|
+
expect(PublicKey.equals(first, second)).toBe(false);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test('PublicKey round-trips through hex', ({ expect }) => {
|
|
30
|
+
const key = PublicKey.random();
|
|
31
|
+
expect(PublicKey.from(key.toHex()).equals(key)).toBe(true);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test('EntityId.random yields valid ids', ({ expect }) => {
|
|
35
|
+
const id = EntityId.random();
|
|
36
|
+
expect(EntityId.isValid(id)).toBe(true);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test('EntityId.deterministic is stable and avoids the platform RNG', ({ expect }) => {
|
|
40
|
+
// Workerd forbids `crypto.getRandomValues()` in global scope, so deterministic() must be
|
|
41
|
+
// reachable at module-eval time — its stability doubles as that guard here.
|
|
42
|
+
const first = EntityId.deterministic('org.dxos.type.person', '0.1.0');
|
|
43
|
+
const second = EntityId.deterministic('org.dxos.type.person', '0.1.0');
|
|
44
|
+
expect(first).toBe(second);
|
|
45
|
+
expect(EntityId.isValid(first)).toBe(true);
|
|
46
|
+
});
|
|
47
|
+
});
|