@dxos/keys 0.9.0 → 0.9.1-main.c7dcc2e112

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-main.c7dcc2e112",
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.9.0",
31
- "@dxos/node-std": "0.9.0",
32
- "@dxos/invariant": "0.9.0"
30
+ "@dxos/invariant": "0.9.1-main.c7dcc2e112",
31
+ "@dxos/debug": "0.9.1-main.c7dcc2e112",
32
+ "@dxos/node-std": "0.9.1-main.c7dcc2e112"
33
33
  },
34
34
  "devDependencies": {
35
35
  "base32-decode": "^1.0.0",
package/src/DXN.test.ts CHANGED
@@ -23,6 +23,40 @@ describe('DXN.isDXN', () => {
23
23
  });
24
24
  });
25
25
 
26
+ describe('DXN.Name', () => {
27
+ test('accepts valid NSIDs — no type errors', () => {
28
+ // Three-segment minimum (first + middle + final), all camelCase.
29
+ DXN.make('a.b.c');
30
+ // Multi-segment, all camelCase.
31
+ DXN.make('org.dxos.type.calendar');
32
+ // Hyphen is allowed in a MIDDLE segment.
33
+ DXN.make('org.dxos.app-framework.event.startup');
34
+ // Versioned form.
35
+ DXN.make('org.dxos.type.calendar', '1.0.0');
36
+ });
37
+
38
+ test('rejects invalid NSIDs — compile-time type errors', () => {
39
+ // Wrapped in a never-called arrow so the invalid calls are type-checked but
40
+ // never executed at runtime. If Name unexpectedly starts accepting any
41
+ // of these, the @ts-expect-error directive itself becomes a build error
42
+ // ("Unused '@ts-expect-error' directive"), causing the CI check to fail.
43
+ void (() => {
44
+ // No dots — single segment.
45
+ // @ts-expect-error
46
+ DXN.make('unknown');
47
+ // Hyphen in the FINAL segment.
48
+ // @ts-expect-error
49
+ DXN.make('com.example.type.registry-entry');
50
+ // Hyphen in the final segment, versioned.
51
+ // @ts-expect-error
52
+ DXN.make('com.example.type.registry-entry', '0.1.0');
53
+ // Common mistake: kebab-case activation event name.
54
+ // @ts-expect-error
55
+ DXN.make('org.dxos.app-framework.event.setup-react-surface');
56
+ });
57
+ });
58
+ });
59
+
26
60
  describe('DXN.make', () => {
27
61
  test('produces unversioned DXN', ({ expect }) => {
28
62
  expect(DXN.make('org.dxos.type.calendar')).toBe('dxn:org.dxos.type.calendar');
@@ -32,9 +66,12 @@ describe('DXN.make', () => {
32
66
  expect(DXN.make('org.dxos.type.calendar', '1.0.0')).toBe('dxn:org.dxos.type.calendar:1.0.0');
33
67
  });
34
68
 
35
- test('throws on invalid NSID', ({ expect }) => {
69
+ test('throws on invalid NSID at runtime', ({ expect }) => {
70
+ // @ts-expect-error intentionally invalid NSIDs — verifying runtime throws
36
71
  expect(() => DXN.make('not-a-valid-nsid')).toThrow();
72
+ // @ts-expect-error
37
73
  expect(() => DXN.make('com.example.type.registry-entry')).toThrow();
74
+ // @ts-expect-error
38
75
  expect(() => DXN.make('com.example.type.registry-entry', '0.1.0')).toThrow();
39
76
  });
40
77
  });
package/src/DXN.ts CHANGED
@@ -30,6 +30,33 @@ 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
+
33
60
  /**
34
61
  * Cheap prefix check — does not validate the full DXN grammar.
35
62
  * Sufficient for narrowing a URI to a DXN.
@@ -40,11 +67,19 @@ export const isDXN = (value: unknown): value is DXN => typeof value === 'string'
40
67
  * Constructs a DXN from an NSID (and optional version). Throws if the result
41
68
  * is not a valid DXN. Use `tryMake` for non-throwing string parsing.
42
69
  *
70
+ * Static NSID strings are validated at compile time via {@link Name}:
71
+ * the final segment must be camelCase (no hyphens). Template-literal strings
72
+ * with a runtime prefix are accepted here but still validated at runtime.
73
+ *
43
74
  * @example make('org.dxos.type.calendar') → 'dxn:org.dxos.type.calendar'
44
75
  * @example make('org.dxos.type.calendar', '1.0.0') → 'dxn:org.dxos.type.calendar:1.0.0'
45
76
  */
46
- export const make = (nsid: string, version?: string): DXN =>
47
- parse(version != null ? `dxn:${nsid}:${version}` : `dxn:${nsid}`);
77
+ export const make: {
78
+ <T extends string>(
79
+ nsid: [Name<T>] extends [never] ? `Invalid NSID "${T}": final segment must be camelCase (no hyphens)` : T,
80
+ version?: string,
81
+ ): DXN;
82
+ } = (nsid: string, version?: string): DXN => parse(version != null ? `dxn:${nsid}:${version}` : `dxn:${nsid}`);
48
83
 
49
84
  /**
50
85
  * Parses a full DXN string. Returns undefined on failure.