@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dxos/keys",
3
- "version": "0.9.0",
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.0",
31
- "@dxos/node-std": "0.9.0",
32
- "@dxos/invariant": "0.9.0"
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.3"
37
+ "effect": "3.21.4"
38
38
  },
39
39
  "peerDependencies": {
40
- "effect": "3.21.3"
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 = (nsid: string, version?: string): DXN =>
47
- parse(version != null ? `dxn:${nsid}:${version}` : `dxn:${nsid}`);
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.