@warpgogol/werkstatt-shared 0.8.0 → 0.8.1

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 (42) hide show
  1. package/package.json +28 -15
  2. package/src/checks/index.ts +1 -0
  3. package/src/checks/result-helpers.ts +60 -6
  4. package/src/checks/suppressions-config.ts +26 -1
  5. package/src/content/system-manifest.ts +8 -0
  6. package/src/ontology/archetypes/index.json +17 -1
  7. package/src/ontology/schemas/page-entry.ts +5 -7
  8. package/src/ontology/schemas/system/manifest.ts +8 -0
  9. package/src/ontology/schemas/system/verification.ts +40 -0
  10. package/src/passport/sign.ts +20 -14
  11. package/src/share/middleware/access-protection.ts +174 -0
  12. package/src/share/middleware/tests/access-protection.test.ts +123 -0
  13. package/src/share/page.ts +4 -2
  14. package/src/share/routes/template-filter.ts +36 -0
  15. package/src/share/routes/tests/template-filter.test.ts +40 -0
  16. package/src/share/scripts/lenis.ts +1 -0
  17. package/src/share/semantic/block-extractors/index.ts +24 -0
  18. package/src/share/semantic/build-page.ts +39 -1
  19. package/src/share/semantic/extract.ts +85 -11
  20. package/src/share/semantic/ids.ts +13 -0
  21. package/src/share/semantic/jsonld/organization.ts +20 -0
  22. package/src/share/semantic/jsonld/video.ts +56 -0
  23. package/src/share/semantic/jsonld/webpage.ts +1 -1
  24. package/src/share/semantic/jsonld.ts +5 -0
  25. package/src/share/semantic/models.ts +9 -0
  26. package/src/share/semantic/organization-profile.ts +3 -2
  27. package/src/share/semantic/page-utils.ts +4 -10
  28. package/src/share/semantic/tests/split-sentences.test.ts +97 -0
  29. package/src/share/slug/heading-slugger.ts +26 -0
  30. package/src/share/slug/index.ts +15 -0
  31. package/src/share/slug/slug-id.ts +22 -0
  32. package/src/share/slug/slug-url.ts +24 -0
  33. package/src/share/slug/strategies.ts +68 -0
  34. package/src/share/slug/tests/slug.test.ts +84 -0
  35. package/src/share/tests/build-page-price-markers.test.ts +3 -0
  36. package/src/share/tests/jsonld-video.test.ts +143 -0
  37. package/src/share/tests/jsonld-webpage.test.ts +69 -0
  38. package/src/share/tests/organization-jsonld.test.ts +52 -0
  39. package/src/share/types/axiom-study.d.ts +10 -2
  40. package/src/share/utility-registry.yaml +36 -0
  41. package/src/surface/blueprint-schema.ts +1 -1
  42. package/src/surface/blueprint-types.ts +1 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@warpgogol/werkstatt-shared",
3
- "version": "0.8.0",
3
+ "version": "0.8.1",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "engines": {
@@ -250,6 +250,10 @@
250
250
  "types": "./src/share/middleware/language-redirect.ts",
251
251
  "default": "./src/share/middleware/language-redirect.ts"
252
252
  },
253
+ "./share/middleware/access-protection": {
254
+ "types": "./src/share/middleware/access-protection.ts",
255
+ "default": "./src/share/middleware/access-protection.ts"
256
+ },
253
257
  "./share/offer-capacity": {
254
258
  "types": "./src/share/offer-capacity.ts",
255
259
  "default": "./src/share/offer-capacity.ts"
@@ -318,6 +322,14 @@
318
322
  "types": "./src/share/shared-context.ts",
319
323
  "default": "./src/share/shared-context.ts"
320
324
  },
325
+ "./share/slug": {
326
+ "types": "./src/share/slug/index.ts",
327
+ "default": "./src/share/slug/index.ts"
328
+ },
329
+ "./share/routes/template-filter": {
330
+ "types": "./src/share/routes/template-filter.ts",
331
+ "default": "./src/share/routes/template-filter.ts"
332
+ },
321
333
  "./share/string-utils": {
322
334
  "types": "./src/share/string-utils.ts",
323
335
  "default": "./src/share/string-utils.ts"
@@ -1331,15 +1343,6 @@
1331
1343
  "default": "./src/share/scripts/index.ts"
1332
1344
  }
1333
1345
  },
1334
- "scripts": {
1335
- "lint": "pnpm exec eslint \"src/**/*.ts\"",
1336
- "typecheck": "pnpm exec tsc -p tsconfig.json --noEmit",
1337
- "build": "pnpm exec tsc -p tsconfig.json --noEmit",
1338
- "build:check": "pnpm exec tsc -p tsconfig.json --noEmit",
1339
- "prepublishOnly": "pnpm exec tsc -p tsconfig.json --noEmit",
1340
- "test": "vitest run",
1341
- "test:watch": "vitest"
1342
- },
1343
1346
  "publishConfig": {
1344
1347
  "access": "public"
1345
1348
  },
@@ -1349,14 +1352,17 @@
1349
1352
  ],
1350
1353
  "dependencies": {
1351
1354
  "@lordicon/element": "^2.3.1",
1352
- "@noble/ed25519": "^3.1.0",
1355
+ "@warpgogol/werkstatt": "*",
1353
1356
  "ajv": "^8.20.0",
1354
1357
  "bs58": "^6.0.0",
1358
+ "@sindresorhus/slugify": "^3.0.0",
1359
+ "cyrillic-to-translit-js": "^3.2.1",
1355
1360
  "expr-eval": "^2.0.2",
1356
- "gray-matter": "^4.0.3",
1361
+ "github-slugger": "^2.0.0",
1357
1362
  "hls.js": "^1.6.16",
1358
1363
  "lenis": "^1.3.25",
1359
1364
  "plyr": "^3.8.4",
1365
+ "gray-matter": "^4.0.3",
1360
1366
  "yaml": "^2.9.0",
1361
1367
  "zod": "^4.4.3"
1362
1368
  },
@@ -1367,7 +1373,7 @@
1367
1373
  "@astrojs/markdown-remark": "^7.0.0",
1368
1374
  "@cloudflare/workers-types": "^5.20260801.1",
1369
1375
  "@types/node": "^24.0.0",
1370
- "@warpgogol/werkstatt": "1.4.0",
1376
+ "@warpgogol/werkstatt": "*",
1371
1377
  "astro": "^7.2.0",
1372
1378
  "eslint": "^10.8.0",
1373
1379
  "fast-check": "^4.9.0",
@@ -1376,5 +1382,12 @@
1376
1382
  "typescript-eslint": "8.65.0",
1377
1383
  "vitest": "^4.1.10"
1378
1384
  },
1379
- "packageManager": "pnpm@11.10.0"
1380
- }
1385
+ "scripts": {
1386
+ "lint": "pnpm exec eslint \"src/**/*.ts\"",
1387
+ "typecheck": "pnpm exec tsc -p tsconfig.json --noEmit",
1388
+ "build": "pnpm exec tsc -p tsconfig.json --noEmit",
1389
+ "build:check": "pnpm exec tsc -p tsconfig.json --noEmit",
1390
+ "test": "vitest run",
1391
+ "test:watch": "vitest"
1392
+ }
1393
+ }
@@ -16,6 +16,7 @@ export {
16
16
  passResult,
17
17
  failResult,
18
18
  resultFromViolations,
19
+ formatCounts,
19
20
  } from "./result-helpers.ts";
20
21
 
21
22
  export * from "./suppressions-config.ts";
@@ -16,16 +16,55 @@ ruleId = command; diagnosticsResult takes rich Diagnostic[] with registered ids.
16
16
  </CHANGE_SUMMARY>
17
17
  */
18
18
 
19
- import type { CheckResult, Diagnostic, KernelCommandResult } from "@warpgogol/werkstatt/kernel";
19
+ import type {
20
+ CheckResult,
21
+ Diagnostic,
22
+ KernelCommandResult,
23
+ KernelNextStep,
24
+ } from "@warpgogol/werkstatt/kernel";
25
+
26
+ function defaultFailNextSteps(command: string): KernelNextStep[] {
27
+ return [
28
+ {
29
+ action: `Fix the error diagnostics reported by ${command} above, then re-run the pipeline`,
30
+ kind: "required",
31
+ },
32
+ ];
33
+ }
34
+
35
+ /**
36
+ * Format error/warning counts with proper pluralization and zero-omission.
37
+ *
38
+ * - `0, 0` → `""` (empty string — caller should omit)
39
+ * - `1, 0` → `"1 error"`
40
+ * - `0, 1` → `"1 warning"`
41
+ * - `2, 0` → `"2 errors"`
42
+ * - `2, 1` → `"2 errors, 1 warning"`
43
+ */
44
+ export function formatCounts(errors: number, warnings: number): string {
45
+ const parts: string[] = [];
46
+ if (errors > 0) {
47
+ parts.push(`${errors} error${errors === 1 ? "" : "s"}`);
48
+ }
49
+ if (warnings > 0) {
50
+ parts.push(`${warnings} warning${warnings === 1 ? "" : "s"}`);
51
+ }
52
+ return parts.join(", ");
53
+ }
20
54
 
21
55
  /**
22
56
  * RFC-0203: canonical result builder. Migrated checks emit `Diagnostic[]` with
23
57
  * registered rule ids instead of bare strings. Exit code is 1 when any
24
58
  * diagnostic is an error; warnings and info do not fail the pipeline.
59
+ *
60
+ * When `nextSteps` are not provided and the result has errors, a default
61
+ * agent-actionable nextStep is emitted: "Fix the error diagnostics … then
62
+ * re-run the pipeline".
25
63
  */
26
64
  export function diagnosticsResult(
27
65
  command: string,
28
66
  diagnostics: Diagnostic[],
67
+ nextSteps?: KernelNextStep[],
29
68
  ): KernelCommandResult<CheckResult> {
30
69
  const summary = {
31
70
  error: diagnostics.filter((d) => d.severity === "error").length,
@@ -34,10 +73,14 @@ export function diagnosticsResult(
34
73
  };
35
74
  const status: CheckResult["status"] =
36
75
  summary.error > 0 ? "fail" : summary.warning > 0 ? "warn" : "pass";
76
+ const resolvedNextSteps =
77
+ nextSteps ?? (summary.error > 0 ? defaultFailNextSteps(command) : undefined);
78
+ const counts = formatCounts(summary.error, summary.warning);
37
79
  return {
38
80
  data: { command, status, diagnostics, summary },
39
81
  exitCode: summary.error > 0 ? 1 : 0,
40
- summary: `${command}: ${summary.error} error(s), ${summary.warning} warning(s)`,
82
+ summary: counts ? `[${command}] ${counts}` : `[${command}]`,
83
+ nextSteps: resolvedNextSteps,
41
84
  };
42
85
  }
43
86
 
@@ -58,11 +101,16 @@ function emptySummary(): CheckResult["summary"] {
58
101
  * Canonical success result. RFC-0203: emits an empty `CheckResult` (no
59
102
  * diagnostics) rather than the legacy `{ command, status, violations }` shape.
60
103
  */
61
- export function passResult(command: string, summary?: string): KernelCommandResult<CheckResult> {
104
+ export function passResult(
105
+ command: string,
106
+ summary?: string,
107
+ nextSteps?: KernelNextStep[],
108
+ ): KernelCommandResult<CheckResult> {
62
109
  return {
63
110
  data: { command, status: "pass", diagnostics: [], summary: emptySummary() },
64
111
  exitCode: 0,
65
- summary: summary ?? `${command}: OK`,
112
+ summary: summary ?? `[${command}] OK`,
113
+ nextSteps,
66
114
  };
67
115
  }
68
116
 
@@ -70,10 +118,14 @@ export function passResult(command: string, summary?: string): KernelCommandResu
70
118
  * Canonical failure result — always exits 1 (preserving the historical
71
119
  * failResult contract). RFC-0203: violation strings become error Diagnostics
72
120
  * keyed by the command name.
121
+ *
122
+ * When `nextSteps` are not provided, a default agent-actionable nextStep is
123
+ * emitted: "Fix the error diagnostics … then re-run the pipeline".
73
124
  */
74
125
  export function failResult(
75
126
  command: string,
76
127
  violations: string[],
128
+ nextSteps?: KernelNextStep[],
77
129
  ): KernelCommandResult<CheckResult> {
78
130
  const diagnostics = violationsToDiagnostics(command, violations);
79
131
  return {
@@ -84,7 +136,8 @@ export function failResult(
84
136
  summary: { error: diagnostics.length, warning: 0, info: 0 },
85
137
  },
86
138
  exitCode: 1,
87
- summary: `${command}: ${violations.length} violation(s)`,
139
+ summary: `[${command}] ${violations.length} violation${violations.length === 1 ? "" : "s"}`,
140
+ nextSteps: nextSteps ?? defaultFailNextSteps(command),
88
141
  };
89
142
  }
90
143
 
@@ -97,6 +150,7 @@ export function failResult(
97
150
  export function resultFromViolations(
98
151
  command: string,
99
152
  violations: string[],
153
+ nextSteps?: KernelNextStep[],
100
154
  ): KernelCommandResult<CheckResult> {
101
- return violations.length > 0 ? failResult(command, violations) : passResult(command);
155
+ return violations.length > 0 ? failResult(command, violations, nextSteps) : passResult(command);
102
156
  }
@@ -17,7 +17,32 @@ import { existsSync, readFileSync } from "node:fs";
17
17
  import { join } from "node:path";
18
18
  import { z } from "zod";
19
19
  import { parse as parseYaml } from "yaml";
20
- import type { Finding } from "@syrokomskyi/axiom-study";
20
+
21
+ /**
22
+ * Minimal Finding shape matching @syrokomskyi/axiom-study's Finding interface.
23
+ * Defined locally so the package typechecks without axiom-study installed.
24
+ * See share/types/axiom-study.d.ts for the full canonical declaration.
25
+ */
26
+ interface Finding {
27
+ findingId: string;
28
+ semanticFingerprint: { algorithm: "sha256"; digest: string; size: number; mediaType: string };
29
+ methodologyId: string;
30
+ ruleId: string;
31
+ affectedSubjectId: string;
32
+ title: string;
33
+ severity: "info" | "low" | "medium" | "high" | "critical";
34
+ evidence: Array<{
35
+ evidenceRef: {
36
+ artifactId: string;
37
+ rootDigest: { algorithm: "sha256"; digest: string; size: number; mediaType: string };
38
+ schema: string;
39
+ };
40
+ selector: string;
41
+ evidenceClass: string;
42
+ }>;
43
+ uncertainty: unknown[];
44
+ extension: Record<string, unknown>;
45
+ }
21
46
 
22
47
  export const suppressionRuleSchema = z.object({
23
48
  ruleId: z.string().min(1),
@@ -9,6 +9,7 @@ Stack-agnostic utility used by both engine and site plugin (RFC-0868).</purpose>
9
9
  </MODULE_CONTRACT>
10
10
  <CHANGE_SUMMARY>
11
11
  <item>RFC-0868: extracted from werkstatt-site/src/content/system-manifest.ts.</item>
12
+ <item>RFC-0911: add seo?.anchorText?.extraStopPhrases for anchor-text stop-list extension.</item>
12
13
  </CHANGE_SUMMARY>
13
14
  */
14
15
 
@@ -101,6 +102,13 @@ export interface SystemManifest {
101
102
  swapOrder?: boolean;
102
103
  };
103
104
  };
105
+ /** RFC-0911: SEO validator configuration extension point. */
106
+ seo?: {
107
+ anchorText?: {
108
+ /** Added to the built-in de/uk stop-list. */
109
+ extraStopPhrases?: Record<string, string[]>;
110
+ };
111
+ };
104
112
  }
105
113
 
106
114
  export interface SystemManifestLoadResult {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "schemaVersion": "1.0.0",
3
- "totalCount": 58,
3
+ "totalCount": 59,
4
4
  "entries": [
5
5
  {
6
6
  "id": "approach",
@@ -218,6 +218,18 @@
218
218
  "sourceFile": "packages/werkstatt-site/src/domain/ontology/archetypes/components/footer-promo.yaml",
219
219
  "layer": "component"
220
220
  },
221
+ {
222
+ "id": "gratitude",
223
+ "displayName": "Gratitude",
224
+ "semanticRole": "client-gratitude",
225
+ "version": "1.0.0",
226
+ "layoutHint": "card-grid",
227
+ "acceptedCosmicNames": [
228
+ "Gonggong"
229
+ ],
230
+ "sourceFile": "packages/werkstatt-site/src/domain/ontology/archetypes/sections/gratitude.yaml",
231
+ "layer": "section"
232
+ },
221
233
  {
222
234
  "id": "hero",
223
235
  "displayName": "Hero",
@@ -704,6 +716,7 @@
704
716
  "article-index",
705
717
  "audience-self-identification",
706
718
  "breadcrumbs",
719
+ "client-gratitude",
707
720
  "client-ownership-disclosure",
708
721
  "consent-gated-chat-launcher",
709
722
  "credits-gallery",
@@ -777,6 +790,7 @@
777
790
  "Elara": "@warpgogol/werkstatt-site/ui/sections/dynamic-status-block",
778
791
  "Atlas": "@warpgogol/werkstatt-site/ui/sections/faq-list",
779
792
  "Dione": "@warpgogol/werkstatt-site/ui/sections/final-cta",
793
+ "Gonggong": "@warpgogol/werkstatt-site/ui/sections/gratitude",
780
794
  "Europa": "@warpgogol/werkstatt-site/ui/sections/hero",
781
795
  "Phobos": "@warpgogol/werkstatt-site/ui/sections/hero-decision-card",
782
796
  "Ganymede": "@warpgogol/werkstatt-site/ui/sections/impact",
@@ -854,6 +868,7 @@
854
868
  "dynamic-status-block": "Elara",
855
869
  "faq-list": "Atlas",
856
870
  "final-cta": "Dione",
871
+ "gratitude": "Gonggong",
857
872
  "hero": "Europa",
858
873
  "hero-decision-card": "Phobos",
859
874
  "impact": "Ganymede",
@@ -935,6 +950,7 @@
935
950
  "Elara": "data-driven-status-indicator",
936
951
  "Atlas": "approach",
937
952
  "Dione": "final-cta",
953
+ "Gonggong": "client-gratitude",
938
954
  "Europa": "hero",
939
955
  "Phobos": "hero",
940
956
  "Ganymede": "impact",
@@ -66,14 +66,12 @@ const visibilityExprSchema: z.ZodType<unknown> = z.lazy(() =>
66
66
  export const BlockEntrySchema = z
67
67
  .object({
68
68
  /**
69
- * Optional stable block identifier for analytics, anchor links, and
70
- * block-level attribution. Must be kebab-case when provided.
71
- * page.block.validate enforces uniqueness within a page.
69
+ * Mandatory stable block identifier for analytics, anchor links, and
70
+ * block-level attribution. Must be kebab-case. RFC-0914 makes this required.
71
+ * page.block.validate enforces uniqueness within a page (B-05).
72
+ * page.blocks.extract.validate enforces presence and format (BLOCK-ID-MISSING, BLOCK-ID-INVALID).
72
73
  */
73
- id: z
74
- .string()
75
- .regex(/^[a-z0-9-]+$/, "Block id must be kebab-case")
76
- .optional(),
74
+ id: z.string().regex(/^[a-z0-9]+(-[a-z0-9]+)*$/, "Block id must be kebab-case"),
77
75
 
78
76
  /**
79
77
  * Cosmic name identifying the section/component for this block.
@@ -17,6 +17,7 @@ import { systemSharedContextSchema, systemGrowthSchema } from "./growth.ts";
17
17
  import { systemReleaseSchema } from "./release.ts";
18
18
  import { systemTextSchema } from "./text.ts";
19
19
  import { systemIntegrationsSchema } from "./integrations.ts";
20
+ import { systemVerificationSchema } from "./verification.ts";
20
21
  import { semanticPageTypeSchema, articleMetadataSchema, pageOutputSchema } from "./page-output.ts";
21
22
 
22
23
  /**
@@ -482,6 +483,13 @@ export const systemManifestSchema = z.object({
482
483
  .optional(),
483
484
  })
484
485
  .optional(),
486
+
487
+ /**
488
+ * Search engine verification config (RFC-0909).
489
+ * Declares per-engine verification method and token.
490
+ * When absent, search.verification.validate emits SEARCH-VERIFY-01.
491
+ */
492
+ verification: systemVerificationSchema.optional(),
485
493
  });
486
494
 
487
495
  export type SystemManifest = z.infer<typeof systemManifestSchema>;
@@ -0,0 +1,40 @@
1
+ /*
2
+ <MODULE_CONTRACT>
3
+ <purpose>Verification block schema (RFC-0909) for the system manifest.
4
+ Declares per-search-engine verification method and token for Google Search Console.</purpose>
5
+ <non-goals>
6
+ <item>Do not introduce app-specific runtime composition or deployment behavior into this reusable package source file.</item>
7
+ </non-goals>
8
+ </MODULE_CONTRACT>
9
+ <CHANGE_SUMMARY>
10
+ <item>RFC-0909: initial verification schema for search engine verification surface.</item>
11
+ </CHANGE_SUMMARY>
12
+ */
13
+
14
+ import { z } from "zod";
15
+
16
+ /**
17
+ * The `verification:` block in src/content/system.md. Declares per-search-engine
18
+ * verification method and token so that search.verification.validate can enforce
19
+ * the declaration and the layout can emit the google-site-verification meta tag.
20
+ *
21
+ * Example src/content/system.md verification block:
22
+ * verification:
23
+ * google:
24
+ * method: dns-txt
25
+ * token: google-site-verification=abc123...
26
+ */
27
+ export const systemVerificationSchema = z.object({
28
+ google: z
29
+ .object({
30
+ method: z.enum(["dns-txt", "meta-tag"]),
31
+ /**
32
+ * dns-txt: full TXT value "google-site-verification=...".
33
+ * meta-tag: content attribute of the google-site-verification meta tag.
34
+ */
35
+ token: z.string().min(1),
36
+ })
37
+ .optional(),
38
+ });
39
+
40
+ export type SystemVerification = z.infer<typeof systemVerificationSchema>;
@@ -1,5 +1,5 @@
1
1
  /* <MODULE_CONTRACT>
2
- <purpose>Facilitates Ed25519 key generation, signing, and verification for verifiable credentials.</purpose>
2
+ <purpose>Facilitates Ed25519 key generation, signing, and verification for verifiable credentials via shared signing core (RFC-0921).</purpose>
3
3
  <non-goals>
4
4
  <item>Do not handle raw key storage or management.</item>
5
5
  <item>Do not perform network operations or external API calls.</item>
@@ -8,6 +8,7 @@
8
8
  </MODULE_CONTRACT>
9
9
  <CHANGE_SUMMARY>
10
10
  <item>Tidied by compass.changesummary.tidy; see git history for prior entries.</item>
11
+ <item>RFC-0921: delegate Ed25519 sign/verify/keygen to shared signing core (@warpgogol/werkstatt/signing). Remove @noble/ed25519 import.</item>
11
12
  </CHANGE_SUMMARY> */
12
13
 
13
14
  /**
@@ -24,8 +25,14 @@
24
25
  * - This module never logs, writes, or returns private key material.
25
26
  */
26
27
 
27
- import * as ed from "@noble/ed25519";
28
28
  import bs58 from "bs58";
29
+ import {
30
+ generateKeyPair as signingGenerateKeyPair,
31
+ signBytes as signingSignBytes,
32
+ verifyBytes as signingVerifyBytes,
33
+ fromHex,
34
+ toHex,
35
+ } from "@warpgogol/werkstatt/signing";
29
36
  import type { VCProof, VerifiableCredential } from "./schema.ts";
30
37
 
31
38
  // ---------------------------------------------------------------------------
@@ -90,12 +97,11 @@ export async function generateKeypair(): Promise<{
90
97
  publicKeyBytes: Uint8Array;
91
98
  publicKeyMultibase: string;
92
99
  }> {
93
- const privateKeyBytes = ed.utils.randomSecretKey();
94
- const publicKeyBytes = await ed.getPublicKeyAsync(privateKeyBytes);
100
+ const keyPair = await signingGenerateKeyPair();
95
101
  return {
96
- privateKeyHex: Buffer.from(privateKeyBytes).toString("hex"),
97
- publicKeyBytes,
98
- publicKeyMultibase: toMultibase(publicKeyBytes),
102
+ privateKeyHex: toHex(keyPair.privateKey),
103
+ publicKeyBytes: keyPair.publicKey,
104
+ publicKeyMultibase: toMultibase(keyPair.publicKey),
99
105
  };
100
106
  }
101
107
 
@@ -110,7 +116,7 @@ export async function generateKeypair(): Promise<{
110
116
  * @param privateKeyHex — 32-byte Ed25519 private key as hex (from env secret)
111
117
  * @param verificationMethod — DID key reference (e.g. "did:web:example.org#key-v1")
112
118
  */
113
- // @ai-invariant: signCredential uses @noble/ed25519. Private keys arrive
119
+ // @ai-invariant: signCredential uses the shared signing core (RFC-0921). Private keys arrive
114
120
  // exclusively as hex env vars — never logged, written to disk, or returned.
115
121
  // The signed output is a W3C VC with Ed25519Signature2020 proof.
116
122
 
@@ -119,9 +125,9 @@ export async function signCredential(
119
125
  privateKeyHex: string,
120
126
  verificationMethod: string,
121
127
  ): Promise<VCProof> {
122
- const privateKeyBytes = new Uint8Array(Buffer.from(privateKeyHex, "hex"));
128
+ const privateKeyBytes = fromHex(privateKeyHex);
123
129
  const message = credentialBytes(subject);
124
- const signatureBytes = await ed.signAsync(message, privateKeyBytes);
130
+ const signatureBytes = await signingSignBytes(privateKeyBytes, message);
125
131
 
126
132
  return {
127
133
  type: "Ed25519Signature2020",
@@ -153,7 +159,7 @@ export async function verifyCredential(
153
159
  const publicKeyBytes = fromMultibase(publicKeyMultibase);
154
160
  const signatureBytes = fromMultibase(proof.proofValue);
155
161
  const message = credentialBytes(subject);
156
- return await ed.verifyAsync(signatureBytes, message, publicKeyBytes);
162
+ return await signingVerifyBytes(publicKeyBytes, message, signatureBytes);
157
163
  } catch {
158
164
  return false;
159
165
  }
@@ -173,8 +179,8 @@ export async function verifyCredential(
173
179
  * @param message — canonical bytes to sign
174
180
  */
175
181
  export async function signBytes(privateKeyHex: string, message: Uint8Array): Promise<string> {
176
- const privateKeyBytes = new Uint8Array(Buffer.from(privateKeyHex, "hex"));
177
- const signatureBytes = await ed.signAsync(message, privateKeyBytes);
182
+ const privateKeyBytes = fromHex(privateKeyHex);
183
+ const signatureBytes = await signingSignBytes(privateKeyBytes, message);
178
184
  return toMultibase(signatureBytes);
179
185
  }
180
186
 
@@ -190,7 +196,7 @@ export async function verifyBytes(
190
196
  try {
191
197
  const publicKeyBytes = fromMultibase(publicKeyMultibase);
192
198
  const signatureBytes = fromMultibase(signatureMultibase);
193
- return await ed.verifyAsync(signatureBytes, message, publicKeyBytes);
199
+ return await signingVerifyBytes(publicKeyBytes, message, signatureBytes);
194
200
  } catch {
195
201
  return false;
196
202
  }