@dxos/util 0.8.4-main.bbf232bc24 → 0.8.4-main.bc2380dfbc

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/util",
3
- "version": "0.8.4-main.bbf232bc24",
3
+ "version": "0.8.4-main.bc2380dfbc",
4
4
  "description": "Temporary bucket for misc functions, which should graduate into separate packages.",
5
5
  "homepage": "https://dxos.org",
6
6
  "bugs": "https://github.com/dxos/dxos/issues",
@@ -8,7 +8,7 @@
8
8
  "type": "git",
9
9
  "url": "https://github.com/dxos/dxos"
10
10
  },
11
- "license": "MIT",
11
+ "license": "FSL-1.1-Apache-2.0",
12
12
  "author": "DXOS.org",
13
13
  "sideEffects": false,
14
14
  "type": "module",
@@ -29,13 +29,13 @@
29
29
  ],
30
30
  "dependencies": {
31
31
  "@hazae41/symbol-dispose-polyfill": "^1.0.2",
32
- "@dxos/debug": "0.8.4-main.bbf232bc24",
33
- "@dxos/keys": "0.8.4-main.bbf232bc24",
34
- "@dxos/invariant": "0.8.4-main.bbf232bc24",
35
- "@dxos/node-std": "0.8.4-main.bbf232bc24"
32
+ "@dxos/debug": "0.8.4-main.bc2380dfbc",
33
+ "@dxos/keys": "0.8.4-main.bc2380dfbc",
34
+ "@dxos/invariant": "0.8.4-main.bc2380dfbc",
35
+ "@dxos/node-std": "0.8.4-main.bc2380dfbc"
36
36
  },
37
37
  "devDependencies": {
38
- "@dxos/crypto": "0.8.4-main.bbf232bc24"
38
+ "@dxos/crypto": "0.8.4-main.bc2380dfbc"
39
39
  },
40
40
  "publishConfig": {
41
41
  "access": "public"
package/src/id.test.ts ADDED
@@ -0,0 +1,109 @@
1
+ //
2
+ // Copyright 2026 DXOS.org
3
+ //
4
+
5
+ import { describe, test } from 'vitest';
6
+
7
+ import { id, isWellFormedId, isWellFormedIdPart } from './id';
8
+
9
+ describe('id', () => {
10
+ test('accepts a three-part id', ({ expect }) => {
11
+ expect(id`com.example.foo`).toBe('com.example.foo');
12
+ });
13
+
14
+ test('accepts a four-part id', ({ expect }) => {
15
+ expect(id`org.dxos.plugin.deck`).toBe('org.dxos.plugin.deck');
16
+ });
17
+
18
+ test('accepts hyphens in authority segments', ({ expect }) => {
19
+ expect(id`my-org.example.foo`).toBe('my-org.example.foo');
20
+ });
21
+
22
+ test('accepts mixed-case name segment', ({ expect }) => {
23
+ expect(id`org.dxos.plugin.deckPlugin`).toBe('org.dxos.plugin.deckPlugin');
24
+ });
25
+
26
+ test('accepts alphanumeric characters in authority segments', ({ expect }) => {
27
+ expect(id`foo123.bar456.qux`).toBe('foo123.bar456.qux');
28
+ });
29
+
30
+ test('interpolates values', ({ expect }) => {
31
+ const namespace = 'org.dxos';
32
+ const name = 'plugin';
33
+ expect(id`${namespace}.${name}.deck`).toBe('org.dxos.plugin.deck');
34
+ });
35
+
36
+ test('throws on empty string', ({ expect }) => {
37
+ expect(() => id``).toThrow(/Invalid id/);
38
+ });
39
+
40
+ test('throws for fewer than three parts', ({ expect }) => {
41
+ expect(() => id`foo`).toThrow(/Invalid id/);
42
+ expect(() => id`foo.bar`).toThrow(/Invalid id/);
43
+ });
44
+
45
+ test('throws when first character is a digit', ({ expect }) => {
46
+ expect(() => id`1foo.bar.baz`).toThrow(/Invalid id/);
47
+ });
48
+
49
+ test('throws on leading or trailing dot', ({ expect }) => {
50
+ expect(() => id`.foo.bar.baz`).toThrow(/Invalid id/);
51
+ expect(() => id`foo.bar.baz.`).toThrow(/Invalid id/);
52
+ });
53
+
54
+ test('throws on consecutive dots', ({ expect }) => {
55
+ expect(() => id`foo..bar`).toThrow(/Invalid id/);
56
+ });
57
+
58
+ test('throws on hyphens in the name segment', ({ expect }) => {
59
+ expect(() => id`org.example.foo-bar`).toThrow(/Invalid id/);
60
+ });
61
+
62
+ test('throws on disallowed characters', ({ expect }) => {
63
+ expect(() => id`foo_bar.baz.qux`).toThrow(/Invalid id/);
64
+ expect(() => id`foo bar.baz.qux`).toThrow(/Invalid id/);
65
+ });
66
+
67
+ test('throws when an interpolated value produces an invalid name segment', ({ expect }) => {
68
+ const bad = '1bad';
69
+ expect(() => id`org.dxos.${bad}`).toThrow(/Invalid id/);
70
+ });
71
+ });
72
+
73
+ describe('isWellFormedId', () => {
74
+ test('matches valid ids', ({ expect }) => {
75
+ expect(isWellFormedId('com.example.foo')).toBe(true);
76
+ expect(isWellFormedId('org.dxos.plugin.deck')).toBe(true);
77
+ expect(isWellFormedId('a1.b2.c3')).toBe(true);
78
+ expect(isWellFormedId('my-org.example.foo')).toBe(true);
79
+ });
80
+
81
+ test('rejects ids with fewer than three parts', ({ expect }) => {
82
+ expect(isWellFormedId('foo')).toBe(false);
83
+ expect(isWellFormedId('foo.bar')).toBe(false);
84
+ });
85
+
86
+ test('rejects invalid ids', ({ expect }) => {
87
+ expect(isWellFormedId('')).toBe(false);
88
+ expect(isWellFormedId('1foo.bar.baz')).toBe(false);
89
+ expect(isWellFormedId('foo.bar.')).toBe(false);
90
+ expect(isWellFormedId('.foo.bar')).toBe(false);
91
+ expect(isWellFormedId('foo..bar.baz')).toBe(false);
92
+ expect(isWellFormedId('org.example.foo-bar')).toBe(false);
93
+ });
94
+ });
95
+
96
+ describe('isWellFormedIdPart', () => {
97
+ test('matches valid name segments', ({ expect }) => {
98
+ expect(isWellFormedIdPart('foo')).toBe(true);
99
+ expect(isWellFormedIdPart('Foo')).toBe(true);
100
+ expect(isWellFormedIdPart('fooBar123')).toBe(true);
101
+ });
102
+
103
+ test('rejects invalid name segments', ({ expect }) => {
104
+ expect(isWellFormedIdPart('foo.bar')).toBe(false);
105
+ expect(isWellFormedIdPart('foo-bar')).toBe(false);
106
+ expect(isWellFormedIdPart('1foo')).toBe(false);
107
+ expect(isWellFormedIdPart('')).toBe(false);
108
+ });
109
+ });
package/src/id.ts ADDED
@@ -0,0 +1,48 @@
1
+ //
2
+ // Copyright 2026 DXOS.org
3
+ //
4
+
5
+ // Name segment: starts with alpha, alphanumeric only (no hyphens), max 63 chars.
6
+ const PART = /^[a-zA-Z][a-zA-Z0-9]{0,62}$/;
7
+
8
+ // Reference: https://atproto.com/specs/nsid
9
+ const ID =
10
+ /^[a-zA-Z]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)+(\.[a-zA-Z]([a-zA-Z0-9]{0,62})?)$/;
11
+
12
+ /**
13
+ * Branded string type for well-formed ids.
14
+ */
15
+ export type Id = string & { readonly __id: unique symbol };
16
+
17
+ /**
18
+ * Tagged template literal that constructs a well-formed, dot-delimited id string.
19
+ * Throws if the resulting string is not well-formed.
20
+ *
21
+ * Follows the AT Protocol NSID convention (https://atproto.com/specs/nsid)
22
+ * Full NSID: authority segments (hyphens allowed) + dot + name segment (no hyphens).
23
+ * - Authority segments: at most 253 characters (including periods), and must contain at least two segments.
24
+ * - Name segment: starts with alpha, alphanumeric only (no hyphens), max 63 chars.
25
+ *
26
+ * @example
27
+ * id`org.dxos.plugin.deck` // 'org.dxos.plugin.deck'
28
+ * id`${ns}.${plugin}.deck` // joins interpolated values
29
+ */
30
+ export function id(strings: TemplateStringsArray, ...values: unknown[]): Id {
31
+ const raw = strings.reduce((out, str, i) => out + str + (i < values.length ? String(values[i]) : ''), '');
32
+ if (!ID.test(raw)) {
33
+ throw new Error(`Invalid id (expected AT Protocol NSID): ${JSON.stringify(raw)}`);
34
+ }
35
+
36
+ return raw as Id;
37
+ }
38
+
39
+ /**
40
+ * Returns true if the given string is a well-formed NSID.
41
+ */
42
+ export const isWellFormedId = (value: string): boolean => ID.test(value);
43
+
44
+ /**
45
+ * Returns true if the given string is a well-formed NSID name segment:
46
+ * `/^[a-zA-Z][a-zA-Z0-9]{0,62}$/` (starts with alpha, alphanumeric only, no dots or hyphens).
47
+ */
48
+ export const isWellFormedIdPart = (value: string): boolean => PART.test(value);
package/src/index.ts CHANGED
@@ -24,6 +24,7 @@ export * from './error-format';
24
24
  export * from './filename';
25
25
  export * from './for-each-async';
26
26
  export * from './human-hash';
27
+ export * from './id';
27
28
  export * from './instance-id';
28
29
  export * from './interval';
29
30
  export * from './join-tables';