@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
package/package.json ADDED
@@ -0,0 +1,80 @@
1
+ {
2
+ "name": "@kashscript/lexicons",
3
+ "version": "0.1.0",
4
+ "description": "Zero-dependency lexicon framework — envelope, schema DSL, registry, validator. Agnostic core; domain content ships as opt-in extensions.",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.js",
12
+ "default": "./dist/index.js"
13
+ },
14
+ "./envelope": {
15
+ "types": "./dist/envelope.d.ts",
16
+ "import": "./dist/envelope.js",
17
+ "default": "./dist/envelope.js"
18
+ },
19
+ "./schema": {
20
+ "types": "./dist/schema.d.ts",
21
+ "import": "./dist/schema.js",
22
+ "default": "./dist/schema.js"
23
+ },
24
+ "./registry": {
25
+ "types": "./dist/registry.d.ts",
26
+ "import": "./dist/registry.js",
27
+ "default": "./dist/registry.js"
28
+ },
29
+ "./engine": {
30
+ "types": "./dist/engine.d.ts",
31
+ "import": "./dist/engine.js",
32
+ "default": "./dist/engine.js"
33
+ },
34
+ "./json-schema": {
35
+ "types": "./dist/json-schema.d.ts",
36
+ "import": "./dist/json-schema.js",
37
+ "default": "./dist/json-schema.js"
38
+ },
39
+ "./semver": {
40
+ "types": "./dist/semver.d.ts",
41
+ "import": "./dist/semver.js",
42
+ "default": "./dist/semver.js"
43
+ },
44
+ "./extensions/social": {
45
+ "types": "./dist/extensions/social.d.ts",
46
+ "import": "./dist/extensions/social.js",
47
+ "default": "./dist/extensions/social.js"
48
+ },
49
+ "./extensions/trade": {
50
+ "types": "./dist/extensions/trade.d.ts",
51
+ "import": "./dist/extensions/trade.js",
52
+ "default": "./dist/extensions/trade.js"
53
+ },
54
+ "./extensions/zkp": {
55
+ "types": "./dist/extensions/zkp.d.ts",
56
+ "import": "./dist/extensions/zkp.js",
57
+ "default": "./dist/extensions/zkp.js"
58
+ }
59
+ },
60
+ "sideEffects": false,
61
+ "files": [
62
+ "dist",
63
+ "src",
64
+ "README.md",
65
+ "LICENSE"
66
+ ],
67
+ "scripts": {
68
+ "build": "tsc -p tsconfig.json",
69
+ "typecheck": "tsc --noEmit",
70
+ "clean": "rm -rf dist .turbo"
71
+ },
72
+ "devDependencies": {
73
+ "typescript": "^5.6.3"
74
+ },
75
+ "license": "Apache-2.0",
76
+ "publishConfig": {
77
+ "access": "public",
78
+ "registry": "https://registry.npmjs.org/"
79
+ }
80
+ }
package/src/engine.ts ADDED
@@ -0,0 +1,141 @@
1
+ /**
2
+ * Validation Engine — pure-function entry points that compose a registry
3
+ * with the schema engine.
4
+ *
5
+ * validateEvent(event, registry)
6
+ * 1. Validate the Kash-Event envelope shape (header + signature; body deferred).
7
+ * 2. Parse the declared Lexicon ID from `header.lexicon`.
8
+ * 3. Look up a forward-compatible registered schema via the registry.
9
+ * 4. Deep-validate `body` against the registered schema.
10
+ *
11
+ * getSchemaDefinition(lexiconId, registry)
12
+ * Returns a JSON-Schema Draft-2020-12 representation of the body schema for
13
+ * a given Lexicon ID. Used by Oreoasis (and any external AI/tooling) to
14
+ * "teach" itself a new data type at runtime without code changes.
15
+ *
16
+ * Both functions are pure and stateless. No network, no crypto, no storage.
17
+ */
18
+
19
+ import { BASE_ENVELOPE_SCHEMA, type KashEvent } from "./envelope";
20
+ import { toJsonSchema, type JsonSchema } from "./json-schema";
21
+ import type { LexiconRegistry } from "./registry";
22
+ import {
23
+ validate,
24
+ type Schema,
25
+ type ValidationError,
26
+ type ValidationResult,
27
+ } from "./schema";
28
+ import { parseLexiconId } from "./semver";
29
+
30
+ /**
31
+ * Full envelope-and-body validation against a registry.
32
+ *
33
+ * Returns the typed `KashEvent<Body>` on success, or a flat list of errors
34
+ * keyed by JSON-pointer-style path on failure. All errors are collected
35
+ * (not fail-fast) so the caller can render a complete diagnostic.
36
+ */
37
+ export function validateEvent<Body = unknown>(
38
+ event: unknown,
39
+ registry: LexiconRegistry,
40
+ ): ValidationResult<KashEvent<Body>> {
41
+ // Stage 1 — envelope shape (header + signature; body permissive).
42
+ const envResult = validate<KashEvent>(BASE_ENVELOPE_SCHEMA, event);
43
+ if (!envResult.ok) return envResult as ValidationResult<KashEvent<Body>>;
44
+
45
+ const env = envResult.value;
46
+
47
+ // Stage 2 — parse declared Lexicon ID.
48
+ const parsed = parseLexiconId(env.header.lexicon);
49
+ if (parsed === null) {
50
+ return failOne(["header", "lexicon"], "unparseable lexicon ID", env.header.lexicon);
51
+ }
52
+
53
+ // Stage 3 — resolve from registry (forward-compatible within major).
54
+ const entry = registry.resolve(parsed.nsid, parsed.semver);
55
+ if (!entry) {
56
+ return failOne(
57
+ ["header", "lexicon"],
58
+ `no registered lexicon supports "${env.header.lexicon}"`,
59
+ env.header.lexicon,
60
+ );
61
+ }
62
+
63
+ // Stage 4 — deep-validate body against the registered schema.
64
+ const bodyResult = validate(entry.bodySchema, env.body);
65
+ if (!bodyResult.ok) {
66
+ const rebased: ValidationError[] = bodyResult.errors.map((e) => ({
67
+ path: ["body", ...e.path],
68
+ message: e.message,
69
+ received: e.received,
70
+ }));
71
+ return { ok: false, errors: rebased };
72
+ }
73
+
74
+ return { ok: true, value: env as KashEvent<Body> };
75
+ }
76
+
77
+ /**
78
+ * Validate a raw body against a registered lexicon — bypasses the envelope.
79
+ * Useful when constructing a record locally before signing it.
80
+ */
81
+ export function validateBody<Body = unknown>(
82
+ lexiconId: string,
83
+ body: unknown,
84
+ registry: LexiconRegistry,
85
+ ): ValidationResult<Body> {
86
+ const parsed = parseLexiconId(lexiconId);
87
+ if (parsed === null) {
88
+ return failOne([], `invalid lexicon ID "${lexiconId}"`, lexiconId);
89
+ }
90
+ const entry = registry.resolve(parsed.nsid, parsed.semver);
91
+ if (!entry) {
92
+ return failOne([], `no registered lexicon supports "${lexiconId}"`, lexiconId);
93
+ }
94
+ return validate<Body>(entry.bodySchema, body);
95
+ }
96
+
97
+ /**
98
+ * Return a JSON-Schema-Draft-2020-12 representation of the registered lexicon.
99
+ *
100
+ * Used by Oreoasis (and any external AI / tooling) to load a previously
101
+ * unknown lexicon's structure at runtime. The returned object includes
102
+ * `$schema`, `$id`, `title`, and `description` metadata.
103
+ *
104
+ * Returns `null` if the Lexicon ID is malformed or no compatible entry is
105
+ * registered.
106
+ */
107
+ export function getSchemaDefinition(
108
+ lexiconId: string,
109
+ registry: LexiconRegistry,
110
+ ): JsonSchema | null {
111
+ const parsed = parseLexiconId(lexiconId);
112
+ if (parsed === null) return null;
113
+ const entry = registry.resolve(parsed.nsid, parsed.semver);
114
+ if (!entry) return null;
115
+
116
+ const body = toJsonSchema(entry.bodySchema);
117
+ const out: Record<string, unknown> = {
118
+ $schema: "https://json-schema.org/draft/2020-12/schema",
119
+ $id: `https://kashscript.com/lexicons/${entry.id}`,
120
+ title: entry.title ?? entry.id,
121
+ description: entry.description ?? `Body schema for ${entry.id}`,
122
+ ...body,
123
+ };
124
+ return out;
125
+ }
126
+
127
+ /** Return the raw internal Schema (not JSON-Schema) for advanced consumers. */
128
+ export function getRawSchema(lexiconId: string, registry: LexiconRegistry): Schema | null {
129
+ const parsed = parseLexiconId(lexiconId);
130
+ if (parsed === null) return null;
131
+ const entry = registry.resolve(parsed.nsid, parsed.semver);
132
+ return entry?.bodySchema ?? null;
133
+ }
134
+
135
+ function failOne(
136
+ path: readonly (string | number)[],
137
+ message: string,
138
+ received: unknown,
139
+ ): { ok: false; errors: readonly ValidationError[] } {
140
+ return { ok: false, errors: [{ path, message, received }] };
141
+ }
@@ -0,0 +1,128 @@
1
+ /**
2
+ * The Kash-Event Envelope.
3
+ *
4
+ * Every record stored in a PDS conforms to this shape. The envelope is
5
+ * lexicon-agnostic — only the `body` shape is governed by a specific lexicon.
6
+ * Signatures are bound by a Domain-Separation Tag (DST) to prevent
7
+ * cross-protocol replay (see Identity Protocol §5.5).
8
+ */
9
+
10
+ import { S, type ObjectSchema, type Schema } from "./schema";
11
+ import {
12
+ KASH_RECORD_DST,
13
+ type CID,
14
+ type DID,
15
+ type Iso8601,
16
+ type KashRecordDST,
17
+ type LexiconId,
18
+ type Multibase,
19
+ type SignatureAlgorithm,
20
+ } from "./types";
21
+
22
+ /* ── interfaces ─────────────────────────────────────────────────── */
23
+
24
+ /** Cryptographic signature produced by a Signer under a `purpose: "active"` key. */
25
+ export interface KashSignature {
26
+ /** verificationMethod id, e.g., "did:kash:5g7q…#sig-1". */
27
+ readonly kid: string;
28
+ /** Must match the verification method's `algorithm` field at verification time. */
29
+ readonly alg: SignatureAlgorithm;
30
+ /** Multibase-encoded signature bytes (z-prefix, base58-btc). */
31
+ readonly sig: Multibase;
32
+ /** Fixed DST: "kash-record-v1\n". Prevents cross-protocol replay. */
33
+ readonly dst: KashRecordDST;
34
+ }
35
+
36
+ /** Header carried by every record, lexicon-agnostic. */
37
+ export interface KashHeader {
38
+ /** Author DID. */
39
+ readonly did: DID;
40
+ /** Lexicon ID — <nsid>@<semver>. */
41
+ readonly lexicon: LexiconId;
42
+ /** Issued-at timestamp (ISO-8601 with offset). */
43
+ readonly iat: Iso8601;
44
+ /** Per-record nonce — 32-128 hex chars (≥128 bits of entropy). */
45
+ readonly nonce: string;
46
+ /** CID of the canonicalized body (computed at envelope-build time). */
47
+ readonly cid: CID;
48
+ /** Optional monotonic sequence within this (did, nsid) collection. */
49
+ readonly seq?: number;
50
+ /** Optional CID of the previous record in the same collection. */
51
+ readonly prev?: CID;
52
+ /** Optional record key — opaque, app-chosen. */
53
+ readonly rkey?: string;
54
+ }
55
+
56
+ /** Standard Kash-Event envelope. Every PDS record IS a KashEvent<Body>. */
57
+ export interface KashEvent<Body = unknown> {
58
+ readonly header: KashHeader;
59
+ readonly body: Body;
60
+ readonly signature: KashSignature;
61
+ }
62
+
63
+ /* ── runtime schemas ────────────────────────────────────────────── */
64
+
65
+ export const SIGNATURE_SCHEMA: ObjectSchema = S.object(
66
+ {
67
+ kid: S.string({ minLength: 1, maxLength: 256 }),
68
+ alg: S.enum("ed25519", "es256", "bls12-381-g2"),
69
+ sig: S.string({ format: "multibase" }),
70
+ dst: S.literal(KASH_RECORD_DST),
71
+ },
72
+ ["kid", "alg", "sig", "dst"],
73
+ );
74
+
75
+ export const HEADER_SCHEMA: ObjectSchema = S.object(
76
+ {
77
+ did: S.string({ format: "did-ks" }),
78
+ lexicon: S.string({ format: "lexicon-id" }),
79
+ iat: S.string({ format: "iso8601" }),
80
+ nonce: S.string({ minLength: 32, maxLength: 128, pattern: /^[0-9a-f]+$/ }),
81
+ cid: S.string({ format: "cid" }),
82
+ seq: S.optional(S.number({ integer: true, min: 0 })),
83
+ prev: S.optional(S.string({ format: "cid" })),
84
+ rkey: S.optional(S.string({ minLength: 1, maxLength: 128 })),
85
+ },
86
+ ["did", "lexicon", "iat", "nonce", "cid"],
87
+ );
88
+
89
+ /** Envelope schema parameterised by the body's schema. */
90
+ export function envelopeSchema(body: Schema): ObjectSchema {
91
+ return S.object(
92
+ {
93
+ header: HEADER_SCHEMA,
94
+ body,
95
+ signature: SIGNATURE_SCHEMA,
96
+ },
97
+ ["header", "body", "signature"],
98
+ );
99
+ }
100
+
101
+ /** Envelope schema that accepts any body (used during pre-lexicon shape validation). */
102
+ export const BASE_ENVELOPE_SCHEMA: ObjectSchema = envelopeSchema(S.any());
103
+
104
+ /* ── shared record-reference shape ───────────────────────────────
105
+ *
106
+ * `RecordRef` points to another record by (CID, AT-URI). It is the
107
+ * canonical cross-record link used by every lexicon that references
108
+ * another record (e.g., social replies/reposts, trade receipts → offers,
109
+ * trade.status → receipt, trade.receipt.linkedPost → social.post).
110
+ *
111
+ * Lives in the framework — not in any extension — so domain content
112
+ * doesn't have to re-invent the shape.
113
+ */
114
+
115
+ export interface RecordRef {
116
+ readonly cid: CID;
117
+ readonly uri: string;
118
+ }
119
+
120
+ export const AT_URI_PATTERN = /^at:\/\/did:kash:[1-9A-HJ-NP-Za-km-z]{32,48}\/[a-z0-9.-]+\/[A-Za-z0-9_-]+$/;
121
+
122
+ export const RECORD_REF_SCHEMA: ObjectSchema = S.object(
123
+ {
124
+ cid: S.string({ format: "cid" }),
125
+ uri: S.string({ pattern: AT_URI_PATTERN }),
126
+ },
127
+ ["cid", "uri"],
128
+ );
@@ -0,0 +1,179 @@
1
+ /**
2
+ * Social Extension — domain lexicons (Indusland-shaped but Indusland-agnostic).
3
+ *
4
+ * kash.social.post@1.0.0 — a post (text, media, optional Bento metadata).
5
+ * kash.social.interaction@1.0.0 — unified like / share / react event.
6
+ * kash.social.follow@1.0.0 — directed relationship between two DIDs.
7
+ *
8
+ * Import path: `@kashscript/lexicons/extensions/social`.
9
+ *
10
+ * This file is an **extension**: the Foundry core never auto-loads it.
11
+ * Consumers explicitly register `SOCIAL_EXTENSION` against their
12
+ * `LexiconRegistry` to opt in to social-domain validation.
13
+ *
14
+ * Data minimisation: every field that is not load-bearing for the specific
15
+ * action is optional.
16
+ */
17
+
18
+ import { RECORD_REF_SCHEMA, type RecordRef } from "../envelope";
19
+ import type { LexiconDef } from "../registry";
20
+ import { S, type ObjectSchema } from "../schema";
21
+
22
+ /* ─────────────────────────────────────────────────────────────────
23
+ * kash.social.post@1.0.0
24
+ * ───────────────────────────────────────────────────────────────── */
25
+
26
+ export const KASH_SOCIAL_POST_NSID = "kash.social.post" as const;
27
+ export const KASH_SOCIAL_POST_VERSION = { major: 1, minor: 0, patch: 0 } as const;
28
+ export const KASH_SOCIAL_POST_ID = "kash.social.post@1.0.0" as const;
29
+
30
+ export type BentoAccent = "sun" | "poppy" | "sky" | "lilac" | "sage" | "rose";
31
+ export type BentoSize = "small" | "medium" | "large";
32
+
33
+ export interface BentoMeta {
34
+ readonly title?: string;
35
+ readonly accent?: BentoAccent;
36
+ readonly size?: BentoSize;
37
+ }
38
+
39
+ export type PostMediaMime =
40
+ | "image/jpeg" | "image/png" | "image/webp" | "image/avif"
41
+ | "video/mp4" | "video/webm"
42
+ | "audio/mpeg" | "audio/ogg";
43
+
44
+ export interface PostMediaRef {
45
+ readonly cid: string;
46
+ readonly mimeType: PostMediaMime;
47
+ readonly altText?: string;
48
+ readonly width?: number;
49
+ readonly height?: number;
50
+ readonly durationMs?: number;
51
+ }
52
+
53
+ export interface KashSocialPostBody {
54
+ readonly text?: string;
55
+ readonly media?: readonly PostMediaRef[];
56
+ readonly bento?: BentoMeta;
57
+ readonly replyTo?: RecordRef;
58
+ readonly repostOf?: RecordRef;
59
+ readonly mentions?: readonly string[];
60
+ readonly tags?: readonly string[];
61
+ readonly lang?: string;
62
+ }
63
+
64
+ const POST_MEDIA_REF: ObjectSchema = S.object(
65
+ {
66
+ cid: S.string({ format: "cid" }),
67
+ mimeType: S.enum(
68
+ "image/jpeg", "image/png", "image/webp", "image/avif",
69
+ "video/mp4", "video/webm",
70
+ "audio/mpeg", "audio/ogg",
71
+ ),
72
+ altText: S.optional(S.string({ maxLength: 420 })),
73
+ width: S.optional(S.number({ integer: true, min: 1, max: 16384 })),
74
+ height: S.optional(S.number({ integer: true, min: 1, max: 16384 })),
75
+ durationMs: S.optional(S.number({ integer: true, min: 0, max: 86_400_000 })),
76
+ },
77
+ ["cid", "mimeType"],
78
+ );
79
+
80
+ const BENTO_META: ObjectSchema = S.object(
81
+ {
82
+ title: S.optional(S.string({ minLength: 1, maxLength: 80 })),
83
+ accent: S.optional(S.enum("sun", "poppy", "sky", "lilac", "sage", "rose")),
84
+ size: S.optional(S.enum("small", "medium", "large")),
85
+ },
86
+ [],
87
+ );
88
+
89
+ export const KASH_SOCIAL_POST_BODY_SCHEMA: ObjectSchema = S.object(
90
+ {
91
+ text: S.optional(S.string({ maxLength: 1000 })),
92
+ media: S.optional(S.array(POST_MEDIA_REF, { maxItems: 8 })),
93
+ bento: S.optional(BENTO_META),
94
+ replyTo: S.optional(RECORD_REF_SCHEMA),
95
+ repostOf: S.optional(RECORD_REF_SCHEMA),
96
+ mentions: S.optional(S.array(S.string({ format: "did-ks" }), { maxItems: 16 })),
97
+ tags: S.optional(S.array(S.string({ minLength: 1, maxLength: 32, pattern: /^[a-z0-9][a-z0-9_-]*$/ }), { maxItems: 16 })),
98
+ lang: S.optional(S.string({ format: "bcp47" })),
99
+ },
100
+ [],
101
+ );
102
+
103
+ /* ─────────────────────────────────────────────────────────────────
104
+ * kash.social.interaction@1.0.0
105
+ * ───────────────────────────────────────────────────────────────── */
106
+
107
+ export const KASH_SOCIAL_INTERACTION_NSID = "kash.social.interaction" as const;
108
+ export const KASH_SOCIAL_INTERACTION_VERSION = { major: 1, minor: 0, patch: 0 } as const;
109
+ export const KASH_SOCIAL_INTERACTION_ID = "kash.social.interaction@1.0.0" as const;
110
+
111
+ export type InteractionType = "like" | "share" | "react";
112
+
113
+ export interface KashSocialInteractionBody {
114
+ readonly type: InteractionType;
115
+ readonly target: RecordRef;
116
+ readonly reaction?: string;
117
+ }
118
+
119
+ export const KASH_SOCIAL_INTERACTION_BODY_SCHEMA: ObjectSchema = S.object(
120
+ {
121
+ type: S.enum("like", "share", "react"),
122
+ target: RECORD_REF_SCHEMA,
123
+ reaction: S.optional(S.string({ minLength: 1, maxLength: 16 })),
124
+ },
125
+ ["type", "target"],
126
+ );
127
+
128
+ /* ─────────────────────────────────────────────────────────────────
129
+ * kash.social.follow@1.0.0
130
+ * ───────────────────────────────────────────────────────────────── */
131
+
132
+ export const KASH_SOCIAL_FOLLOW_NSID = "kash.social.follow" as const;
133
+ export const KASH_SOCIAL_FOLLOW_VERSION = { major: 1, minor: 0, patch: 0 } as const;
134
+ export const KASH_SOCIAL_FOLLOW_ID = "kash.social.follow@1.0.0" as const;
135
+
136
+ export interface KashSocialFollowBody {
137
+ readonly subject: string;
138
+ readonly subjectHandle?: string;
139
+ }
140
+
141
+ export const KASH_SOCIAL_FOLLOW_BODY_SCHEMA: ObjectSchema = S.object(
142
+ {
143
+ subject: S.string({ format: "did-ks" }),
144
+ subjectHandle: S.optional(S.string({ minLength: 1, maxLength: 64 })),
145
+ },
146
+ ["subject"],
147
+ );
148
+
149
+ /* ─────────────────────────────────────────────────────────────────
150
+ * Extension bundle
151
+ * ───────────────────────────────────────────────────────────────── */
152
+
153
+ /** All Social-domain lexicons. Pass to `LexiconRegistry.registerAll()`. */
154
+ export const SOCIAL_EXTENSION: readonly LexiconDef[] = [
155
+ {
156
+ id: KASH_SOCIAL_POST_ID,
157
+ nsid: KASH_SOCIAL_POST_NSID,
158
+ semver: KASH_SOCIAL_POST_VERSION,
159
+ bodySchema: KASH_SOCIAL_POST_BODY_SCHEMA,
160
+ title: "Social Post",
161
+ description: "A post — text, media, and optional Bento-style tile metadata.",
162
+ },
163
+ {
164
+ id: KASH_SOCIAL_INTERACTION_ID,
165
+ nsid: KASH_SOCIAL_INTERACTION_NSID,
166
+ semver: KASH_SOCIAL_INTERACTION_VERSION,
167
+ bodySchema: KASH_SOCIAL_INTERACTION_BODY_SCHEMA,
168
+ title: "Social Interaction",
169
+ description: "Unified like / share / react event against a post.",
170
+ },
171
+ {
172
+ id: KASH_SOCIAL_FOLLOW_ID,
173
+ nsid: KASH_SOCIAL_FOLLOW_NSID,
174
+ semver: KASH_SOCIAL_FOLLOW_VERSION,
175
+ bodySchema: KASH_SOCIAL_FOLLOW_BODY_SCHEMA,
176
+ title: "Social Follow",
177
+ description: "Directed follow relationship between two DIDs.",
178
+ },
179
+ ];