@dxos/keys 0.9.0 → 0.9.1-staging.ee54ba693a
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 +7 -0
- package/dist/lib/browser/index.mjs.map +3 -3
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/node-esm/index.mjs +7 -0
- 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 +30 -1
- package/dist/types/src/DXN.d.ts.map +1 -1
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/package.json +6 -6
- package/src/DXN.test.ts +55 -1
- package/src/DXN.ts +48 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dxos/keys",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.1-staging.ee54ba693a",
|
|
4
4
|
"description": "Key utils and definitions.",
|
|
5
5
|
"homepage": "https://dxos.org",
|
|
6
6
|
"bugs": "https://github.com/dxos/dxos/issues",
|
|
@@ -27,17 +27,17 @@
|
|
|
27
27
|
],
|
|
28
28
|
"dependencies": {
|
|
29
29
|
"ulidx": "^2.3.0",
|
|
30
|
-
"@dxos/debug": "0.9.
|
|
31
|
-
"@dxos/
|
|
32
|
-
"@dxos/
|
|
30
|
+
"@dxos/debug": "0.9.1-staging.ee54ba693a",
|
|
31
|
+
"@dxos/invariant": "0.9.1-staging.ee54ba693a",
|
|
32
|
+
"@dxos/node-std": "0.9.1-staging.ee54ba693a"
|
|
33
33
|
},
|
|
34
34
|
"devDependencies": {
|
|
35
35
|
"base32-decode": "^1.0.0",
|
|
36
36
|
"base32-encode": "^2.0.0",
|
|
37
|
-
"effect": "3.21.
|
|
37
|
+
"effect": "3.21.4"
|
|
38
38
|
},
|
|
39
39
|
"peerDependencies": {
|
|
40
|
-
"effect": "3.21.
|
|
40
|
+
"effect": "3.21.4"
|
|
41
41
|
},
|
|
42
42
|
"publishConfig": {
|
|
43
43
|
"access": "public"
|
package/src/DXN.test.ts
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
// Copyright 2025 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
+
import * as Schema from 'effect/Schema';
|
|
5
6
|
import { describe, test } from 'vitest';
|
|
6
7
|
|
|
7
8
|
import * as DXN from './DXN';
|
|
@@ -23,6 +24,40 @@ describe('DXN.isDXN', () => {
|
|
|
23
24
|
});
|
|
24
25
|
});
|
|
25
26
|
|
|
27
|
+
describe('DXN.Name', () => {
|
|
28
|
+
test('accepts valid NSIDs — no type errors', () => {
|
|
29
|
+
// Three-segment minimum (first + middle + final), all camelCase.
|
|
30
|
+
DXN.make('a.b.c');
|
|
31
|
+
// Multi-segment, all camelCase.
|
|
32
|
+
DXN.make('org.dxos.type.calendar');
|
|
33
|
+
// Hyphen is allowed in a MIDDLE segment.
|
|
34
|
+
DXN.make('org.dxos.app-framework.event.startup');
|
|
35
|
+
// Versioned form.
|
|
36
|
+
DXN.make('org.dxos.type.calendar', '1.0.0');
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test('rejects invalid NSIDs — compile-time type errors', () => {
|
|
40
|
+
// Wrapped in a never-called arrow so the invalid calls are type-checked but
|
|
41
|
+
// never executed at runtime. If Name unexpectedly starts accepting any
|
|
42
|
+
// of these, the @ts-expect-error directive itself becomes a build error
|
|
43
|
+
// ("Unused '@ts-expect-error' directive"), causing the CI check to fail.
|
|
44
|
+
void (() => {
|
|
45
|
+
// No dots — single segment.
|
|
46
|
+
// @ts-expect-error
|
|
47
|
+
DXN.make('unknown');
|
|
48
|
+
// Hyphen in the FINAL segment.
|
|
49
|
+
// @ts-expect-error
|
|
50
|
+
DXN.make('com.example.type.registry-entry');
|
|
51
|
+
// Hyphen in the final segment, versioned.
|
|
52
|
+
// @ts-expect-error
|
|
53
|
+
DXN.make('com.example.type.registry-entry', '0.1.0');
|
|
54
|
+
// Common mistake: kebab-case activation event name.
|
|
55
|
+
// @ts-expect-error
|
|
56
|
+
DXN.make('org.dxos.app-framework.event.setup-react-surface');
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
|
|
26
61
|
describe('DXN.make', () => {
|
|
27
62
|
test('produces unversioned DXN', ({ expect }) => {
|
|
28
63
|
expect(DXN.make('org.dxos.type.calendar')).toBe('dxn:org.dxos.type.calendar');
|
|
@@ -32,9 +67,12 @@ describe('DXN.make', () => {
|
|
|
32
67
|
expect(DXN.make('org.dxos.type.calendar', '1.0.0')).toBe('dxn:org.dxos.type.calendar:1.0.0');
|
|
33
68
|
});
|
|
34
69
|
|
|
35
|
-
test('throws on invalid NSID', ({ expect }) => {
|
|
70
|
+
test('throws on invalid NSID at runtime', ({ expect }) => {
|
|
71
|
+
// @ts-expect-error intentionally invalid NSIDs — verifying runtime throws
|
|
36
72
|
expect(() => DXN.make('not-a-valid-nsid')).toThrow();
|
|
73
|
+
// @ts-expect-error
|
|
37
74
|
expect(() => DXN.make('com.example.type.registry-entry')).toThrow();
|
|
75
|
+
// @ts-expect-error
|
|
38
76
|
expect(() => DXN.make('com.example.type.registry-entry', '0.1.0')).toThrow();
|
|
39
77
|
});
|
|
40
78
|
});
|
|
@@ -83,3 +121,19 @@ describe('DXN.getVersion', () => {
|
|
|
83
121
|
expect(DXN.getVersion(DXN.make('org.dxos.type.calendar'))).toBeUndefined();
|
|
84
122
|
});
|
|
85
123
|
});
|
|
124
|
+
|
|
125
|
+
describe('DXN.NameSchema', () => {
|
|
126
|
+
const isName = Schema.is(DXN.NameSchema);
|
|
127
|
+
|
|
128
|
+
test('accepts a well-formed NSID name (no dxn: prefix)', ({ expect }) => {
|
|
129
|
+
expect(isName('com.anthropic.model.claude-sonnet-4-6.default')).toBe(true);
|
|
130
|
+
expect(isName('org.dxos.provider.edge')).toBe(true);
|
|
131
|
+
expect(isName('com.meta.model.llama-3-2-1b.instruct')).toBe(true);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
test('rejects malformed names', ({ expect }) => {
|
|
135
|
+
expect(isName('single')).toBe(false); // not multi-segment
|
|
136
|
+
expect(isName('com.example.model.has-hyphen')).toBe(false); // final segment has a hyphen
|
|
137
|
+
expect(isName('dxn:com.example.type.thing')).toBe(false); // already a full DXN, not a bare name
|
|
138
|
+
});
|
|
139
|
+
});
|
package/src/DXN.ts
CHANGED
|
@@ -30,6 +30,44 @@ const DXN_SPEC_REGEXP =
|
|
|
30
30
|
*/
|
|
31
31
|
export type DXN = URI.URI & { readonly __DXN: unique symbol };
|
|
32
32
|
|
|
33
|
+
/**
|
|
34
|
+
* Compile-time validation for NSID strings (the `dxn:` prefix is absent here).
|
|
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.
|
|
39
|
+
*
|
|
40
|
+
* TypeScript template literal inference is non-greedy: `${string}.${infer Rest}`
|
|
41
|
+
* always splits at the first dot. The type recurses until `Rest` has no more dots,
|
|
42
|
+
* at which point it is the true final segment and is checked for hyphens.
|
|
43
|
+
*
|
|
44
|
+
* Broad `string` passes through unchanged so that template-literal call sites
|
|
45
|
+
* whose prefix segment is `string` are not rejected — those are validated at
|
|
46
|
+
* runtime by the regex inside `parse`.
|
|
47
|
+
*/
|
|
48
|
+
export type Name<T extends string> = [string] extends [T]
|
|
49
|
+
? string
|
|
50
|
+
: T extends `${string}.${infer Rest}`
|
|
51
|
+
? Rest extends `${string}.${string}`
|
|
52
|
+
? [Name<Rest>] extends [never]
|
|
53
|
+
? never
|
|
54
|
+
: T
|
|
55
|
+
: Rest extends `${string}-${string}`
|
|
56
|
+
? never
|
|
57
|
+
: T
|
|
58
|
+
: never;
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Effect Schema validating an NSID name — the `dxn:`-less portion — at runtime, mirroring the rules
|
|
62
|
+
* the {@link Name} type checks at compile time (multi-segment; camelCase final segment). Pairs with
|
|
63
|
+
* the {@link Name} type for schema fields that hold a bare NSID (e.g. a model id passed to a creator
|
|
64
|
+
* helper). Named `NameSchema` because a value cannot share the generic `Name` type's name.
|
|
65
|
+
*/
|
|
66
|
+
export const NameSchema: Schema.Schema<string, string> = Schema.String.pipe(
|
|
67
|
+
Schema.filter((value) => DXN_SPEC_REGEXP.test(`dxn:${value}`), { message: () => 'Invalid NSID name' }),
|
|
68
|
+
Schema.annotations({ title: 'DXN.Name', description: 'NSID name (the dxn: prefix omitted)' }),
|
|
69
|
+
);
|
|
70
|
+
|
|
33
71
|
/**
|
|
34
72
|
* Cheap prefix check — does not validate the full DXN grammar.
|
|
35
73
|
* Sufficient for narrowing a URI to a DXN.
|
|
@@ -40,11 +78,19 @@ export const isDXN = (value: unknown): value is DXN => typeof value === 'string'
|
|
|
40
78
|
* Constructs a DXN from an NSID (and optional version). Throws if the result
|
|
41
79
|
* is not a valid DXN. Use `tryMake` for non-throwing string parsing.
|
|
42
80
|
*
|
|
81
|
+
* Static NSID strings are validated at compile time via {@link Name}:
|
|
82
|
+
* the final segment must be camelCase (no hyphens). Template-literal strings
|
|
83
|
+
* with a runtime prefix are accepted here but still validated at runtime.
|
|
84
|
+
*
|
|
43
85
|
* @example make('org.dxos.type.calendar') → 'dxn:org.dxos.type.calendar'
|
|
44
86
|
* @example make('org.dxos.type.calendar', '1.0.0') → 'dxn:org.dxos.type.calendar:1.0.0'
|
|
45
87
|
*/
|
|
46
|
-
export const make
|
|
47
|
-
|
|
88
|
+
export const make: {
|
|
89
|
+
<T extends string>(
|
|
90
|
+
nsid: [Name<T>] extends [never] ? `Invalid NSID "${T}": final segment must be camelCase (no hyphens)` : T,
|
|
91
|
+
version?: string,
|
|
92
|
+
): DXN;
|
|
93
|
+
} = (nsid: string, version?: string): DXN => parse(version != null ? `dxn:${nsid}:${version}` : `dxn:${nsid}`);
|
|
48
94
|
|
|
49
95
|
/**
|
|
50
96
|
* Parses a full DXN string. Returns undefined on failure.
|