@kashscript/lexicons 0.1.0

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.
Files changed (78) hide show
  1. package/LICENSE +199 -0
  2. package/README.md +73 -0
  3. package/dist/engine.d.ts +48 -0
  4. package/dist/engine.d.ts.map +1 -0
  5. package/dist/engine.js +110 -0
  6. package/dist/engine.js.map +1 -0
  7. package/dist/envelope.d.ts +59 -0
  8. package/dist/envelope.d.ts.map +1 -0
  9. package/dist/envelope.js +43 -0
  10. package/dist/envelope.js.map +1 -0
  11. package/dist/extensions/social.d.ts +82 -0
  12. package/dist/extensions/social.d.ts.map +1 -0
  13. package/dist/extensions/social.js +99 -0
  14. package/dist/extensions/social.js.map +1 -0
  15. package/dist/extensions/trade.d.ts +94 -0
  16. package/dist/extensions/trade.d.ts.map +1 -0
  17. package/dist/extensions/trade.js +116 -0
  18. package/dist/extensions/trade.js.map +1 -0
  19. package/dist/extensions/zkp.d.ts +76 -0
  20. package/dist/extensions/zkp.d.ts.map +1 -0
  21. package/dist/extensions/zkp.js +68 -0
  22. package/dist/extensions/zkp.js.map +1 -0
  23. package/dist/index.d.ts +26 -0
  24. package/dist/index.d.ts.map +1 -0
  25. package/dist/index.js +28 -0
  26. package/dist/index.js.map +1 -0
  27. package/dist/json-schema.d.ts +18 -0
  28. package/dist/json-schema.d.ts.map +1 -0
  29. package/dist/json-schema.js +125 -0
  30. package/dist/json-schema.js.map +1 -0
  31. package/dist/lexicons/index.d.ts +32 -0
  32. package/dist/lexicons/index.d.ts.map +1 -0
  33. package/dist/lexicons/index.js +59 -0
  34. package/dist/lexicons/index.js.map +1 -0
  35. package/dist/lexicons/kash-id-audit.d.ts +32 -0
  36. package/dist/lexicons/kash-id-audit.d.ts.map +1 -0
  37. package/dist/lexicons/kash-id-audit.js +21 -0
  38. package/dist/lexicons/kash-id-audit.js.map +1 -0
  39. package/dist/lexicons/kash-id-preferences.d.ts +60 -0
  40. package/dist/lexicons/kash-id-preferences.d.ts.map +1 -0
  41. package/dist/lexicons/kash-id-preferences.js +43 -0
  42. package/dist/lexicons/kash-id-preferences.js.map +1 -0
  43. package/dist/lexicons/kash-id-profile.d.ts +46 -0
  44. package/dist/lexicons/kash-id-profile.d.ts.map +1 -0
  45. package/dist/lexicons/kash-id-profile.js +31 -0
  46. package/dist/lexicons/kash-id-profile.js.map +1 -0
  47. package/dist/registry.d.ts +78 -0
  48. package/dist/registry.d.ts.map +1 -0
  49. package/dist/registry.js +133 -0
  50. package/dist/registry.js.map +1 -0
  51. package/dist/schema.d.ts +101 -0
  52. package/dist/schema.d.ts.map +1 -0
  53. package/dist/schema.js +224 -0
  54. package/dist/schema.js.map +1 -0
  55. package/dist/semver.d.ts +29 -0
  56. package/dist/semver.d.ts.map +1 -0
  57. package/dist/semver.js +42 -0
  58. package/dist/semver.js.map +1 -0
  59. package/dist/types.d.ts +21 -0
  60. package/dist/types.d.ts.map +1 -0
  61. package/dist/types.js +8 -0
  62. package/dist/types.js.map +1 -0
  63. package/package.json +80 -0
  64. package/src/engine.ts +141 -0
  65. package/src/envelope.ts +128 -0
  66. package/src/extensions/social.ts +179 -0
  67. package/src/extensions/trade.ts +210 -0
  68. package/src/extensions/zkp.ts +108 -0
  69. package/src/index.ts +30 -0
  70. package/src/json-schema.ts +141 -0
  71. package/src/lexicons/index.ts +68 -0
  72. package/src/lexicons/kash-id-audit.ts +61 -0
  73. package/src/lexicons/kash-id-preferences.ts +114 -0
  74. package/src/lexicons/kash-id-profile.ts +82 -0
  75. package/src/registry.ts +168 -0
  76. package/src/schema.ts +347 -0
  77. package/src/semver.ts +56 -0
  78. package/src/types.ts +27 -0
@@ -0,0 +1,114 @@
1
+ /**
2
+ * Lexicon: kash.id.preferences@1.0.0
3
+ *
4
+ * App-agnostic user preferences. Single-record collection (rkey: "self").
5
+ *
6
+ * Privacy-by-default: if `privacy.allowAI` is absent or any flag within it
7
+ * is absent, the relevant flag defaults to FALSE. Oreoasis MUST NOT ingest
8
+ * a user's records without `allowAI.neuronIngest === true`.
9
+ */
10
+
11
+ import { S, type ObjectSchema } from "../schema";
12
+
13
+ export const KASH_PREFERENCES_NSID = "kash.id.preferences" as const;
14
+ export const KASH_PREFERENCES_VERSION = { major: 1, minor: 0, patch: 0 } as const;
15
+ export const KASH_PREFERENCES_ID = "kash.id.preferences@1.0.0" as const;
16
+
17
+ /* ── types ──────────────────────────────────────────────────────── */
18
+
19
+ export type ThemePref = "light" | "dark" | "system";
20
+ export type Visibility = "public" | "followers" | "private";
21
+ export type TimeFormat = "12h" | "24h";
22
+
23
+ export interface AIConsent {
24
+ /** May my records be used to train models? */
25
+ readonly training: boolean;
26
+ /** May my records be retrieved at inference time (RAG)? */
27
+ readonly inference: boolean;
28
+ /** May Oreoasis (Neuron) ingest my records into its context engine? */
29
+ readonly neuronIngest: boolean;
30
+ /** Optional list of NSIDs explicitly excluded from any AI use. */
31
+ readonly excludedCollections?: readonly string[];
32
+ }
33
+
34
+ export interface PrivacyPreferences {
35
+ /** Visibility of the profile to non-followers / public crawlers. */
36
+ readonly profileVisibility: Visibility;
37
+ /** Whether crawlers / federation peers may index this identity. */
38
+ readonly indexable: boolean;
39
+ /** AI consent flags — ALL DEFAULT FALSE if absent. */
40
+ readonly allowAI?: AIConsent;
41
+ }
42
+
43
+ export interface NotificationPreferences {
44
+ readonly handshakes: boolean;
45
+ readonly keyRotations: boolean;
46
+ readonly quorumActivity: boolean;
47
+ }
48
+
49
+ export interface UnitPreferences {
50
+ /** ISO 4217 currency code (USD, PKR, EUR, …). */
51
+ readonly currency: string;
52
+ readonly timeFormat: TimeFormat;
53
+ /** strftime-like template ("%Y-%m-%d"). */
54
+ readonly dateFormat: string;
55
+ }
56
+
57
+ export interface KashPreferencesBody {
58
+ /** BCP 47 language tag ("en", "ur-PK"). */
59
+ readonly language?: string;
60
+ readonly theme?: ThemePref;
61
+ readonly privacy?: PrivacyPreferences;
62
+ readonly notifications?: NotificationPreferences;
63
+ readonly units?: UnitPreferences;
64
+ }
65
+
66
+ /* ── runtime schema ─────────────────────────────────────────────── */
67
+
68
+ const AI_CONSENT: ObjectSchema = S.object(
69
+ {
70
+ training: S.boolean(),
71
+ inference: S.boolean(),
72
+ neuronIngest: S.boolean(),
73
+ excludedCollections: S.optional(S.array(S.string({ format: "nsid" }), { maxItems: 64 })),
74
+ },
75
+ ["training", "inference", "neuronIngest"],
76
+ );
77
+
78
+ const PRIVACY: ObjectSchema = S.object(
79
+ {
80
+ profileVisibility: S.enum("public", "followers", "private"),
81
+ indexable: S.boolean(),
82
+ allowAI: S.optional(AI_CONSENT),
83
+ },
84
+ ["profileVisibility", "indexable"],
85
+ );
86
+
87
+ const NOTIFICATIONS: ObjectSchema = S.object(
88
+ {
89
+ handshakes: S.boolean(),
90
+ keyRotations: S.boolean(),
91
+ quorumActivity: S.boolean(),
92
+ },
93
+ ["handshakes", "keyRotations", "quorumActivity"],
94
+ );
95
+
96
+ const UNITS: ObjectSchema = S.object(
97
+ {
98
+ currency: S.string({ format: "iso4217" }),
99
+ timeFormat: S.enum("12h", "24h"),
100
+ dateFormat: S.string({ minLength: 1, maxLength: 32 }),
101
+ },
102
+ ["currency", "timeFormat", "dateFormat"],
103
+ );
104
+
105
+ export const KASH_PREFERENCES_BODY_SCHEMA: ObjectSchema = S.object(
106
+ {
107
+ language: S.optional(S.string({ format: "bcp47" })),
108
+ theme: S.optional(S.enum("light", "dark", "system")),
109
+ privacy: S.optional(PRIVACY),
110
+ notifications: S.optional(NOTIFICATIONS),
111
+ units: S.optional(UNITS),
112
+ },
113
+ [],
114
+ );
@@ -0,0 +1,82 @@
1
+ /**
2
+ * Lexicon: kash.id.profile@1.0.0
3
+ *
4
+ * Sovereign user profile. Single-record collection (rkey: "self" by convention).
5
+ */
6
+
7
+ import { S, type ObjectSchema } from "../schema";
8
+ import type { CID } from "../types";
9
+
10
+ export const KASH_PROFILE_NSID = "kash.id.profile" as const;
11
+ export const KASH_PROFILE_VERSION = { major: 1, minor: 0, patch: 0 } as const;
12
+ export const KASH_PROFILE_ID = "kash.id.profile@1.0.0" as const;
13
+
14
+ /* ── types ──────────────────────────────────────────────────────── */
15
+
16
+ export type ProfileImageMime = "image/jpeg" | "image/png" | "image/webp" | "image/avif";
17
+
18
+ export interface ProfileBlobRef {
19
+ readonly cid: CID;
20
+ readonly mimeType: ProfileImageMime;
21
+ readonly altText?: string;
22
+ }
23
+
24
+ export interface ProfileLink {
25
+ readonly label: string;
26
+ readonly url: string;
27
+ }
28
+
29
+ export interface KashProfileBody {
30
+ /** Optional human-readable handle hint. The authoritative handle is in the DID Document's alsoKnownAs. */
31
+ readonly handle?: string;
32
+ /** Display name. 1–64 characters after NFC normalisation. */
33
+ readonly displayName: string;
34
+ /** Short bio. 0–280 characters. */
35
+ readonly bio?: string;
36
+ /** Avatar image blob reference. */
37
+ readonly avatar?: ProfileBlobRef;
38
+ /** Banner image blob reference. */
39
+ readonly banner?: ProfileBlobRef;
40
+ /** Free-form pronouns string. */
41
+ readonly pronouns?: string;
42
+ /** Human-readable location ("Islamabad, PK"). */
43
+ readonly location?: string;
44
+ /** Pinned PDS / relay URLs the author wants peers to discover. Max 8. */
45
+ readonly pdsUrls?: readonly string[];
46
+ /** External profile links. Max 8. */
47
+ readonly links?: readonly ProfileLink[];
48
+ }
49
+
50
+ /* ── runtime schema ─────────────────────────────────────────────── */
51
+
52
+ const PROFILE_BLOB_REF: ObjectSchema = S.object(
53
+ {
54
+ cid: S.string({ format: "cid" }),
55
+ mimeType: S.enum("image/jpeg", "image/png", "image/webp", "image/avif"),
56
+ altText: S.optional(S.string({ maxLength: 420 })),
57
+ },
58
+ ["cid", "mimeType"],
59
+ );
60
+
61
+ const PROFILE_LINK: ObjectSchema = S.object(
62
+ {
63
+ label: S.string({ minLength: 1, maxLength: 32 }),
64
+ url: S.string({ format: "url" }),
65
+ },
66
+ ["label", "url"],
67
+ );
68
+
69
+ export const KASH_PROFILE_BODY_SCHEMA: ObjectSchema = S.object(
70
+ {
71
+ handle: S.optional(S.string({ minLength: 1, maxLength: 64 })),
72
+ displayName: S.string({ minLength: 1, maxLength: 64 }),
73
+ bio: S.optional(S.string({ maxLength: 280 })),
74
+ avatar: S.optional(PROFILE_BLOB_REF),
75
+ banner: S.optional(PROFILE_BLOB_REF),
76
+ pronouns: S.optional(S.string({ maxLength: 32 })),
77
+ location: S.optional(S.string({ maxLength: 64 })),
78
+ pdsUrls: S.optional(S.array(S.string({ format: "url" }), { maxItems: 8 })),
79
+ links: S.optional(S.array(PROFILE_LINK, { maxItems: 8 })),
80
+ },
81
+ ["displayName"],
82
+ );
@@ -0,0 +1,168 @@
1
+ /**
2
+ * LexiconRegistry — agnostic storage for Lexicon definitions.
3
+ *
4
+ * The registry holds a map of Lexicon IDs (NSID@semver) to schema definitions.
5
+ * It is **empty by default**: no Identity, Social, or Trade lexicons are
6
+ * pre-registered. Consumers explicitly register a Standard Library or load
7
+ * Extensions at bootstrap time (see `./engine.ts` and the Standard Library /
8
+ * Extension exports).
9
+ *
10
+ * The class does ZERO validation work itself — it just stores and resolves.
11
+ * Validation is performed by `validateEvent` (see `./engine.ts`) which
12
+ * accepts a registry as a parameter. This decoupling preserves the
13
+ * Box-vs-Content separation: the registry (Box) never knows about any
14
+ * specific domain (Content).
15
+ */
16
+
17
+ import type { Schema } from "./schema";
18
+ import { compareSemver, isForwardCompatible, parseLexiconId, type Semver } from "./semver";
19
+
20
+ /** A single lexicon definition held in the registry. */
21
+ export interface LexiconDef {
22
+ /** Full Lexicon ID — e.g., "kash.id.profile@1.0.0". */
23
+ readonly id: string;
24
+ /** NSID portion — e.g., "kash.id.profile". */
25
+ readonly nsid: string;
26
+ /** Semver of this definition. */
27
+ readonly semver: Semver;
28
+ /** Schema for the envelope body. */
29
+ readonly bodySchema: Schema;
30
+ /** Optional human-readable title (shown by tooling). */
31
+ readonly title?: string;
32
+ /** Optional human-readable description (shown by tooling). */
33
+ readonly description?: string;
34
+ }
35
+
36
+ /** Options accepted by the `register(id, schema, opts)` convenience overload. */
37
+ export interface RegisterOptions {
38
+ readonly title?: string;
39
+ readonly description?: string;
40
+ }
41
+
42
+ /**
43
+ * In-memory map of registered lexicons.
44
+ *
45
+ * Multiple major versions for the same NSID may co-exist. The registry
46
+ * resolves by exact match first, then by forward-compatible major.
47
+ */
48
+ export class LexiconRegistry {
49
+ private readonly byId: Map<string, LexiconDef> = new Map();
50
+ private readonly byNsid: Map<string, LexiconDef[]> = new Map();
51
+
52
+ /** Number of registered definitions. */
53
+ get size(): number {
54
+ return this.byId.size;
55
+ }
56
+
57
+ /** Register a fully-described lexicon entry. */
58
+ register(entry: LexiconDef): this;
59
+ /** Register by Lexicon ID + body schema (convenience overload). */
60
+ register(id: string, bodySchema: Schema, opts?: RegisterOptions): this;
61
+ register(arg1: LexiconDef | string, arg2?: Schema, arg3?: RegisterOptions): this {
62
+ if (typeof arg1 === "string") {
63
+ if (arg2 === undefined) {
64
+ throw new TypeError("LexiconRegistry.register(id, schema): schema is required");
65
+ }
66
+ const parsed = parseLexiconId(arg1);
67
+ if (parsed === null) {
68
+ throw new TypeError(`LexiconRegistry.register: invalid lexicon ID "${arg1}"`);
69
+ }
70
+ const entry: LexiconDef = {
71
+ id: arg1,
72
+ nsid: parsed.nsid,
73
+ semver: parsed.semver,
74
+ bodySchema: arg2,
75
+ ...(arg3?.title !== undefined ? { title: arg3.title } : {}),
76
+ ...(arg3?.description !== undefined ? { description: arg3.description } : {}),
77
+ };
78
+ return this.addEntry(entry);
79
+ }
80
+ return this.addEntry(arg1);
81
+ }
82
+
83
+ /** Register a batch of entries (typically a Standard Library or Extension export). */
84
+ registerAll(entries: readonly LexiconDef[]): this {
85
+ for (const entry of entries) this.addEntry(entry);
86
+ return this;
87
+ }
88
+
89
+ /** Exact-match lookup by full Lexicon ID. Returns undefined when not present. */
90
+ get(id: string): LexiconDef | undefined {
91
+ return this.byId.get(id);
92
+ }
93
+
94
+ /**
95
+ * Forward-compatible lookup by NSID + caller-supplied semver.
96
+ * Returns the highest-version registered entry whose major matches the caller.
97
+ */
98
+ resolve(nsid: string, semver: Semver): LexiconDef | undefined {
99
+ const candidates = this.byNsid.get(nsid);
100
+ if (!candidates) return undefined;
101
+ for (const def of candidates) {
102
+ if (isForwardCompatible(def.semver, semver)) return def;
103
+ }
104
+ return undefined;
105
+ }
106
+
107
+ /** True if any version of the given NSID is registered. */
108
+ has(nsid: string): boolean {
109
+ return this.byNsid.has(nsid);
110
+ }
111
+
112
+ /** True if the exact Lexicon ID is registered. */
113
+ hasId(id: string): boolean {
114
+ return this.byId.has(id);
115
+ }
116
+
117
+ /** All registered Lexicon IDs, in insertion order. */
118
+ list(): readonly string[] {
119
+ return Array.from(this.byId.keys());
120
+ }
121
+
122
+ /** All registered entries. */
123
+ entries(): readonly LexiconDef[] {
124
+ return Array.from(this.byId.values());
125
+ }
126
+
127
+ /** All NSIDs present in the registry. */
128
+ namespaces(): readonly string[] {
129
+ return Array.from(this.byNsid.keys());
130
+ }
131
+
132
+ /** Drop every registered entry. */
133
+ clear(): void {
134
+ this.byId.clear();
135
+ this.byNsid.clear();
136
+ }
137
+
138
+ /** Remove a single entry by its full Lexicon ID. Returns whether anything was removed. */
139
+ unregister(id: string): boolean {
140
+ const entry = this.byId.get(id);
141
+ if (!entry) return false;
142
+ this.byId.delete(id);
143
+ const list = this.byNsid.get(entry.nsid);
144
+ if (list) {
145
+ const next = list.filter((e) => e.id !== id);
146
+ if (next.length === 0) this.byNsid.delete(entry.nsid);
147
+ else this.byNsid.set(entry.nsid, next);
148
+ }
149
+ return true;
150
+ }
151
+
152
+ /* ── internal ─────────────────────────────────────────────────── */
153
+
154
+ private addEntry(entry: LexiconDef): this {
155
+ this.byId.set(entry.id, entry);
156
+ const list = this.byNsid.get(entry.nsid);
157
+ if (list) {
158
+ // remove any existing entry with the same full ID, then re-insert sorted descending
159
+ const next = list.filter((e) => e.id !== entry.id);
160
+ next.push(entry);
161
+ next.sort((a, b) => compareSemver(b.semver, a.semver));
162
+ this.byNsid.set(entry.nsid, next);
163
+ } else {
164
+ this.byNsid.set(entry.nsid, [entry]);
165
+ }
166
+ return this;
167
+ }
168
+ }