@phosra/cli 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 (58) hide show
  1. package/README.md +42 -0
  2. package/dist/bin.d.ts +19 -0
  3. package/dist/bin.d.ts.map +1 -0
  4. package/dist/bin.js +159 -0
  5. package/dist/bin.js.map +1 -0
  6. package/dist/commands/caps.d.ts +16 -0
  7. package/dist/commands/caps.d.ts.map +1 -0
  8. package/dist/commands/caps.js +125 -0
  9. package/dist/commands/caps.js.map +1 -0
  10. package/dist/commands/doctor.d.ts +34 -0
  11. package/dist/commands/doctor.d.ts.map +1 -0
  12. package/dist/commands/doctor.js +321 -0
  13. package/dist/commands/doctor.js.map +1 -0
  14. package/dist/commands/gk-check.d.ts +29 -0
  15. package/dist/commands/gk-check.d.ts.map +1 -0
  16. package/dist/commands/gk-check.js +134 -0
  17. package/dist/commands/gk-check.js.map +1 -0
  18. package/dist/commands/init.d.ts +18 -0
  19. package/dist/commands/init.d.ts.map +1 -0
  20. package/dist/commands/init.js +90 -0
  21. package/dist/commands/init.js.map +1 -0
  22. package/dist/commands/link-write.d.ts +26 -0
  23. package/dist/commands/link-write.d.ts.map +1 -0
  24. package/dist/commands/link-write.js +129 -0
  25. package/dist/commands/link-write.js.map +1 -0
  26. package/dist/commands/register.d.ts +63 -0
  27. package/dist/commands/register.d.ts.map +1 -0
  28. package/dist/commands/register.js +167 -0
  29. package/dist/commands/register.js.map +1 -0
  30. package/dist/config.d.ts +72 -0
  31. package/dist/config.d.ts.map +1 -0
  32. package/dist/config.js +94 -0
  33. package/dist/config.js.map +1 -0
  34. package/dist/envelope.d.ts +32 -0
  35. package/dist/envelope.d.ts.map +1 -0
  36. package/dist/envelope.js +53 -0
  37. package/dist/envelope.js.map +1 -0
  38. package/dist/keygen.d.ts +29 -0
  39. package/dist/keygen.d.ts.map +1 -0
  40. package/dist/keygen.js +38 -0
  41. package/dist/keygen.js.map +1 -0
  42. package/dist/out.d.ts +33 -0
  43. package/dist/out.d.ts.map +1 -0
  44. package/dist/out.js +76 -0
  45. package/dist/out.js.map +1 -0
  46. package/dist/round-trip.d.ts +51 -0
  47. package/dist/round-trip.d.ts.map +1 -0
  48. package/dist/round-trip.js +115 -0
  49. package/dist/round-trip.js.map +1 -0
  50. package/dist/sender-key.d.ts +26 -0
  51. package/dist/sender-key.d.ts.map +1 -0
  52. package/dist/sender-key.js +35 -0
  53. package/dist/sender-key.js.map +1 -0
  54. package/dist/transport.d.ts +30 -0
  55. package/dist/transport.d.ts.map +1 -0
  56. package/dist/transport.js +62 -0
  57. package/dist/transport.js.map +1 -0
  58. package/package.json +31 -0
@@ -0,0 +1 @@
1
+ {"version":3,"file":"link-write.d.ts","sourceRoot":"","sources":["../../src/commands/link-write.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAGH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAEjD,MAAM,WAAW,aAAa;IAC5B,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,OAAO,GAAG,MAAM,GAAG,OAAO,CAAC;IACrC,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,OAAO,CAAC;CACf;AAED,wBAAsB,YAAY,CAChC,GAAG,EAAE,YAAY,EACjB,IAAI,EAAE,aAAa,GAClB,OAAO,CAAC,IAAI,CAAC,CA6Gf"}
@@ -0,0 +1,129 @@
1
+ /**
2
+ * phosra link write <category> — provider rule-write via the @phosra/link SDK.
3
+ *
4
+ * This is the "metered binding leg": the provider signs a rule on behalf of a
5
+ * parent's signed consent, writing it to the census. Requires:
6
+ * - PHOSRA_DATABASE_URL — Postgres connection string for the @phosra/link grant store
7
+ * - PHOSRA_GRANT_ID — active grant ID produced by the connect ceremony
8
+ * - PHOSRA_CHILD_REF — target child ref (e.g. "child:<uuid>")
9
+ *
10
+ * §12.3: the census is the safety authority. This command drives the signed
11
+ * census write; it does NOT accept a rule, sign a trust-list, or hold billing
12
+ * authority.
13
+ *
14
+ * @phosra/link is a peer / optional dependency — the CLI core only requires
15
+ * @openchildsafety/ocss. If @phosra/link is not installed this command will prompt for it.
16
+ */
17
+ import { printJson } from "../out.js";
18
+ export async function runLinkWrite(cfg, opts) {
19
+ // ── guard: DATABASE_URL required ─────────────────────────────────────────
20
+ if (!cfg.databaseUrl) {
21
+ const msg = "PHOSRA_DATABASE_URL is required for `phosra link write` — it is the " +
22
+ "Postgres connection string for the @phosra/link grant store.\n\n" +
23
+ " Set it in .phosra.env:\n" +
24
+ " PHOSRA_DATABASE_URL=postgresql://...\n\n" +
25
+ " Or in the shell:\n" +
26
+ " export PHOSRA_DATABASE_URL=postgresql://...\n";
27
+ if (opts.json) {
28
+ printJson({ ok: false, error: "missing_database_url", hint: msg });
29
+ }
30
+ else {
31
+ console.error(" ✘ " + msg);
32
+ }
33
+ process.exit(1);
34
+ }
35
+ // ── dynamic import of @phosra/link ──────────────────────────────────────
36
+ let linkModule;
37
+ try {
38
+ linkModule = await import("@phosra/link");
39
+ }
40
+ catch {
41
+ const msg = "`phosra link write` requires @phosra/link.\n\n" +
42
+ " Install it:\n" +
43
+ " npm install @phosra/link\n" +
44
+ " or (workspace):\n" +
45
+ " npm install -w @phosra/cli @phosra/link\n";
46
+ if (opts.json) {
47
+ printJson({ ok: false, error: "missing_peer_dep", package: "@phosra/link", hint: msg });
48
+ }
49
+ else {
50
+ console.error(" ✘ " + msg);
51
+ }
52
+ process.exit(1);
53
+ }
54
+ // ── dynamic import of pg ─────────────────────────────────────────────────
55
+ let pgModule;
56
+ try {
57
+ pgModule = await import("pg");
58
+ }
59
+ catch {
60
+ if (opts.json) {
61
+ printJson({ ok: false, error: "missing_peer_dep", package: "pg" });
62
+ }
63
+ else {
64
+ console.error(" ✘ `phosra link write` requires pg. Run: npm install pg");
65
+ }
66
+ process.exit(1);
67
+ }
68
+ const { Pool } = pgModule.default ?? pgModule;
69
+ const { directive } = linkModule;
70
+ // ── build config ─────────────────────────────────────────────────────────
71
+ const { senderKeyFromSeedB64url } = await import("../sender-key.js");
72
+ const { HOUSEHOLD_ACME_KEY_ID } = await import("../config.js");
73
+ const parentKey = senderKeyFromSeedB64url(cfg.householdSeed, HOUSEHOLD_ACME_KEY_ID);
74
+ // Writer key: in the CLI context the writer IS the parent persona
75
+ // (the CLI doesn't hold a separate provider signing key — it drives the
76
+ // parent's own rule-write for sandbox demos, not a provider-on-behalf-of-parent flow).
77
+ const pool = new Pool({ connectionString: cfg.databaseUrl });
78
+ const linkCfg = {
79
+ censusBaseUrl: cfg.censusUrl,
80
+ trustRootXB64Url: cfg.trustRootX,
81
+ parentKey,
82
+ writerKey: parentKey,
83
+ writerDid: "did:ocss:household-acme",
84
+ routerDid: "did:ocss:phosra-router",
85
+ householdSecret: cfg.householdSeed, // seed doubles as secret in sandbox
86
+ pool,
87
+ };
88
+ let res;
89
+ try {
90
+ res = await directive(linkCfg, opts.grantId, opts.category, opts.childRef, {
91
+ decision: opts.decision,
92
+ });
93
+ }
94
+ catch (err) {
95
+ await pool.end();
96
+ if (opts.json) {
97
+ printJson({ ok: false, error: "directive_failed", message: err.message });
98
+ }
99
+ else {
100
+ console.error(` ✘ directive failed: ${err.message}`);
101
+ }
102
+ process.exit(1);
103
+ }
104
+ await pool.end();
105
+ const body = await res.text();
106
+ let parsed;
107
+ try {
108
+ parsed = JSON.parse(body);
109
+ }
110
+ catch {
111
+ parsed = body;
112
+ }
113
+ if (opts.json) {
114
+ printJson({ ok: res.ok, http_status: res.status, body: parsed });
115
+ }
116
+ else {
117
+ const icon = res.ok ? "✔" : "✘";
118
+ console.log(` ${icon} HTTP ${res.status} ${opts.category} → ${opts.decision}`);
119
+ if (!res.ok) {
120
+ console.error(`\n ${JSON.stringify(parsed, null, 2)}`);
121
+ }
122
+ else {
123
+ console.log(`\n ${JSON.stringify(parsed, null, 2)}`);
124
+ }
125
+ }
126
+ if (!res.ok)
127
+ process.exit(1);
128
+ }
129
+ //# sourceMappingURL=link-write.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"link-write.js","sourceRoot":"","sources":["../../src/commands/link-write.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AAWtC,MAAM,CAAC,KAAK,UAAU,YAAY,CAChC,GAAiB,EACjB,IAAmB;IAEnB,4EAA4E;IAC5E,IAAI,CAAC,GAAG,CAAC,WAAW,EAAE,CAAC;QACrB,MAAM,GAAG,GACP,sEAAsE;YACtE,kEAAkE;YAClE,4BAA4B;YAC5B,8CAA8C;YAC9C,sBAAsB;YACtB,mDAAmD,CAAC;QACtD,IAAI,IAAI,CAAC,IAAI,EAAE,CAAC;YACd,SAAS,CAAC,EAAE,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,sBAAsB,EAAE,IAAI,EAAE,GAAG,EAAE,CAAC,CAAC;QACrE,CAAC;aAAM,CAAC;YACN,OAAO,CAAC,KAAK,CAAC,OAAO,GAAG,GAAG,CAAC,CAAC;QAC/B,CAAC;QACD,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC;IAED,2EAA2E;IAC3E,IAAI,UAAyC,CAAC;IAC9C,IAAI,CAAC;QACH,UAAU,GAAG,MAAM,MAAM,CAAC,cAAc,CAAC,CAAC;IAC5C,CAAC;IAAC,MAAM,CAAC;QACP,MAAM,GAAG,GACP,gDAAgD;YAChD,iBAAiB;YACjB,gCAAgC;YAChC,qBAAqB;YACrB,+CAA+C,CAAC;QAClD,IAAI,IAAI,CAAC,IAAI,EAAE,CAAC;YACd,SAAS,CAAC,EAAE,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,kBAAkB,EAAE,OAAO,EAAE,cAAc,EAAE,IAAI,EAAE,GAAG,EAAE,CAAC,CAAC;QAC1F,CAAC;aAAM,CAAC;YACN,OAAO,CAAC,KAAK,CAAC,OAAO,GAAG,GAAG,CAAC,CAAC;QAC/B,CAAC;QACD,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC;IAED,4EAA4E;IAC5E,IAAI,QAA6B,CAAC;IAClC,IAAI,CAAC;QACH,QAAQ,GAAG,MAAM,MAAM,CAAC,IAAI,CAAC,CAAC;IAChC,CAAC;IAAC,MAAM,CAAC;QACP,IAAI,IAAI,CAAC,IAAI,EAAE,CAAC;YACd,SAAS,CAAC,EAAE,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,kBAAkB,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC,CAAC;QACrE,CAAC;aAAM,CAAC;YACN,OAAO,CAAC,KAAK,CAAC,4DAA4D,CAAC,CAAC;QAC9E,CAAC;QACD,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC;IAED,MAAM,EAAE,IAAI,EAAE,GAAG,QAAQ,CAAC,OAAO,IAAI,QAAQ,CAAC;IAC9C,MAAM,EAAE,SAAS,EAAE,GAAG,UAAU,CAAC;IAEjC,4EAA4E;IAC5E,MAAM,EAAE,uBAAuB,EAAE,GAAG,MAAM,MAAM,CAAC,kBAAkB,CAAC,CAAC;IACrE,MAAM,EAAE,qBAAqB,EAAE,GAAG,MAAM,MAAM,CAAC,cAAc,CAAC,CAAC;IAE/D,MAAM,SAAS,GAAG,uBAAuB,CAAC,GAAG,CAAC,aAAa,EAAE,qBAAqB,CAAC,CAAC;IAEpF,kEAAkE;IAClE,wEAAwE;IACxE,uFAAuF;IACvF,MAAM,IAAI,GAAG,IAAI,IAAI,CAAC,EAAE,gBAAgB,EAAE,GAAG,CAAC,WAAW,EAAE,CAAC,CAAC;IAE7D,MAAM,OAAO,GAAG;QACd,aAAa,EAAG,GAAG,CAAC,SAAS;QAC7B,gBAAgB,EAAE,GAAG,CAAC,UAAU;QAChC,SAAS;QACT,SAAS,EAAO,SAAS;QACzB,SAAS,EAAO,yBAAyB;QACzC,SAAS,EAAO,wBAAwB;QACxC,eAAe,EAAE,GAAG,CAAC,aAAa,EAAI,oCAAoC;QAC1E,IAAI;KACL,CAAC;IAEF,IAAI,GAAa,CAAC;IAClB,IAAI,CAAC;QACH,GAAG,GAAG,MAAM,SAAS,CAAC,OAAO,EAAE,IAAI,CAAC,OAAO,EAAE,IAAI,CAAC,QAAQ,EAAE,IAAI,CAAC,QAAQ,EAAE;YACzE,QAAQ,EAAE,IAAI,CAAC,QAAQ;SACxB,CAAC,CAAC;IACL,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,MAAM,IAAI,CAAC,GAAG,EAAE,CAAC;QACjB,IAAI,IAAI,CAAC,IAAI,EAAE,CAAC;YACd,SAAS,CAAC,EAAE,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,kBAAkB,EAAE,OAAO,EAAG,GAAa,CAAC,OAAO,EAAE,CAAC,CAAC;QACvF,CAAC;aAAM,CAAC;YACN,OAAO,CAAC,KAAK,CAAC,0BAA2B,GAAa,CAAC,OAAO,EAAE,CAAC,CAAC;QACpE,CAAC;QACD,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC;IAED,MAAM,IAAI,CAAC,GAAG,EAAE,CAAC;IAEjB,MAAM,IAAI,GAAG,MAAM,GAAG,CAAC,IAAI,EAAE,CAAC;IAC9B,IAAI,MAAe,CAAC;IACpB,IAAI,CAAC;QAAC,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;IAAC,CAAC;IAAC,MAAM,CAAC;QAAC,MAAM,GAAG,IAAI,CAAC;IAAC,CAAC;IAE3D,IAAI,IAAI,CAAC,IAAI,EAAE,CAAC;QACd,SAAS,CAAC,EAAE,EAAE,EAAE,GAAG,CAAC,EAAE,EAAE,WAAW,EAAE,GAAG,CAAC,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC,CAAC;IACnE,CAAC;SAAM,CAAC;QACN,MAAM,IAAI,GAAG,GAAG,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC;QAChC,OAAO,CAAC,GAAG,CAAC,KAAK,IAAI,UAAU,GAAG,CAAC,MAAM,KAAK,IAAI,CAAC,QAAQ,MAAM,IAAI,CAAC,QAAQ,EAAE,CAAC,CAAC;QAClF,IAAI,CAAC,GAAG,CAAC,EAAE,EAAE,CAAC;YACZ,OAAO,CAAC,KAAK,CAAC,OAAO,IAAI,CAAC,SAAS,CAAC,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC,EAAE,CAAC,CAAC;QAC1D,CAAC;aAAM,CAAC;YACN,OAAO,CAAC,GAAG,CAAC,OAAO,IAAI,CAAC,SAAS,CAAC,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC,EAAE,CAAC,CAAC;QACxD,CAAC;IACH,CAAC;IAED,IAAI,CAAC,GAAG,CAAC,EAAE;QAAE,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAC/B,CAAC"}
@@ -0,0 +1,63 @@
1
+ /**
2
+ * phosra register — Phase 2a self-serve SANDBOX provider onboarding.
3
+ *
4
+ * Workflow:
5
+ * 1. Generate a fresh Ed25519 keypair client-side (NEVER transmitted as a secret)
6
+ * 2. POST {did, public_key_b64url, roles} to /api/v1/advisors/self-register
7
+ * 3. Verify the new DID appears on the live trust-list immediately
8
+ * 4. Write a .phosra.env file with the party's OWN seed + DID
9
+ * 5. Prove a green round-trip: consent_attestation → rule write → receipt
10
+ *
11
+ * §12.3 clean: the CLI DRIVES the census (self-registers, proves) but carries
12
+ * NO authority. Self-register is gated by SANDBOX_MODE=true on the census.
13
+ * The generated seed is written to the env file ONCE; it has the same secret
14
+ * threat model as any API key.
15
+ *
16
+ * NOTE: "no household-acme" means the cold party's DID is NOT household-acme.
17
+ * household-acme still acts as the CONSENT ISSUER (parent persona) for the
18
+ * rule-write standing — that is its designed sandbox role. The registered
19
+ * party is the WRITER (rule_write writer_ref = this party's DID).
20
+ */
21
+ export interface RegisterOpts {
22
+ /** Census base URL (no trailing slash). Defaults to staging sandbox. */
23
+ apiUrl?: string;
24
+ /** Trust-list root public key X (base64url-raw). Defaults to staging sandbox. */
25
+ trustRootX?: string;
26
+ /** Roles to request (informational for provisional entries). Default ["editor"]. */
27
+ roles?: string[];
28
+ /** Path to write the .phosra.env file. Default: .phosra.env in cwd. */
29
+ out?: string;
30
+ /** Party slug override (lowercase alphanum + -). Default: random p<hex>. */
31
+ slug?: string;
32
+ /** Household-acme seed for the consent issuer. Default: staging sandbox default. */
33
+ householdSeed?: string;
34
+ /** Mia policy ID for the round-trip proof. Default: staging sandbox default. */
35
+ miaPolicyId?: string;
36
+ /** Mia child ID for the round-trip proof. Default: staging sandbox default. */
37
+ miaChildId?: string;
38
+ /** Injectable fetch for testing. */
39
+ fetchImpl?: typeof fetch;
40
+ /** Injectable clock for testing. */
41
+ now?: () => number;
42
+ }
43
+ export interface RegisterResult {
44
+ /** The newly-registered DID (e.g. "did:ocss:p3a7f1c9"). */
45
+ PHOSRA_DID: string;
46
+ /** Base64url Ed25519 seed for this party's key. KEEP SECRET. */
47
+ PHOSRA_SENDER_SEED_B64URL: string;
48
+ /** Census URL the DID was registered against. */
49
+ PHOSRA_CENSUS_URL: string;
50
+ /** Trust-list root X used. */
51
+ PHOSRA_TRUST_ROOT_X: string;
52
+ /** Path to the written .phosra.env file. */
53
+ envFile: string;
54
+ }
55
+ /**
56
+ * runRegister — the `phosra register` implementation.
57
+ *
58
+ * Generates a fresh DID, self-registers it on the sandbox census, verifies it
59
+ * on the trust-list, and proves a rule-write round-trip WITHOUT impersonating
60
+ * household-acme as the writer.
61
+ */
62
+ export declare function runRegister(opts?: RegisterOpts): Promise<RegisterResult>;
63
+ //# sourceMappingURL=register.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"register.d.ts","sourceRoot":"","sources":["../../src/commands/register.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAiCH,MAAM,WAAW,YAAY;IAC3B,wEAAwE;IACxE,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,iFAAiF;IACjF,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,oFAAoF;IACpF,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC;IACjB,uEAAuE;IACvE,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,4EAA4E;IAC5E,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,oFAAoF;IACpF,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,gFAAgF;IAChF,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,+EAA+E;IAC/E,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,oCAAoC;IACpC,SAAS,CAAC,EAAE,OAAO,KAAK,CAAC;IACzB,oCAAoC;IACpC,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,cAAc;IAC7B,2DAA2D;IAC3D,UAAU,EAAE,MAAM,CAAC;IACnB,gEAAgE;IAChE,yBAAyB,EAAE,MAAM,CAAC;IAClC,iDAAiD;IACjD,iBAAiB,EAAE,MAAM,CAAC;IAC1B,8BAA8B;IAC9B,mBAAmB,EAAE,MAAM,CAAC;IAC5B,4CAA4C;IAC5C,OAAO,EAAE,MAAM,CAAC;CACjB;AAED;;;;;;GAMG;AACH,wBAAsB,WAAW,CAAC,IAAI,GAAE,YAAiB,GAAG,OAAO,CAAC,cAAc,CAAC,CAqJlF"}
@@ -0,0 +1,167 @@
1
+ /**
2
+ * phosra register — Phase 2a self-serve SANDBOX provider onboarding.
3
+ *
4
+ * Workflow:
5
+ * 1. Generate a fresh Ed25519 keypair client-side (NEVER transmitted as a secret)
6
+ * 2. POST {did, public_key_b64url, roles} to /api/v1/advisors/self-register
7
+ * 3. Verify the new DID appears on the live trust-list immediately
8
+ * 4. Write a .phosra.env file with the party's OWN seed + DID
9
+ * 5. Prove a green round-trip: consent_attestation → rule write → receipt
10
+ *
11
+ * §12.3 clean: the CLI DRIVES the census (self-registers, proves) but carries
12
+ * NO authority. Self-register is gated by SANDBOX_MODE=true on the census.
13
+ * The generated seed is written to the env file ONCE; it has the same secret
14
+ * threat model as any API key.
15
+ *
16
+ * NOTE: "no household-acme" means the cold party's DID is NOT household-acme.
17
+ * household-acme still acts as the CONSENT ISSUER (parent persona) for the
18
+ * rule-write standing — that is its designed sandbox role. The registered
19
+ * party is the WRITER (rule_write writer_ref = this party's DID).
20
+ */
21
+ import { writeFileSync } from "node:fs";
22
+ import { join } from "node:path";
23
+ import { randomBytes } from "node:crypto";
24
+ import { verifyDocument, fromVerifiedDocument, } from "@openchildsafety/ocss";
25
+ import { generateKey } from "../keygen.js";
26
+ import { senderKeyFromSeedB64url } from "../sender-key.js";
27
+ import { CensusTransport } from "../transport.js";
28
+ import { buildConsentAttestation } from "../envelope.js";
29
+ import { SANDBOX_DEFAULTS, HOUSEHOLD_ACME_KEY_ID, ROUTER_DID, ROUNDTRIP_APP_DID, ROUNDTRIP_RULE_CATEGORY, } from "../config.js";
30
+ // ── helpers ────────────────────────────────────────────────────────────────────
31
+ /** Generate a random lowercase alphanumeric slug safe for did:ocss use. */
32
+ function randomSlug() {
33
+ // 8 lowercase hex chars → 4 bytes entropy → plenty for a sandbox party DID.
34
+ return "p" + randomBytes(4).toString("hex"); // e.g. "p3a7f1c9"
35
+ }
36
+ /**
37
+ * runRegister — the `phosra register` implementation.
38
+ *
39
+ * Generates a fresh DID, self-registers it on the sandbox census, verifies it
40
+ * on the trust-list, and proves a rule-write round-trip WITHOUT impersonating
41
+ * household-acme as the writer.
42
+ */
43
+ export async function runRegister(opts = {}) {
44
+ const censusUrl = opts.apiUrl ?? SANDBOX_DEFAULTS.CENSUS_URL;
45
+ const trustRootX = opts.trustRootX ?? SANDBOX_DEFAULTS.TRUST_ROOT_X;
46
+ const roles = opts.roles ?? ["editor"];
47
+ const outFile = opts.out ?? join(process.cwd(), ".phosra.env");
48
+ const slug = opts.slug ?? randomSlug();
49
+ const householdSeed = opts.householdSeed ?? SANDBOX_DEFAULTS.HOUSEHOLD_SEED;
50
+ const miaPolicyId = opts.miaPolicyId ?? SANDBOX_DEFAULTS.MIA_POLICY_ID;
51
+ const miaChildId = opts.miaChildId ?? SANDBOX_DEFAULTS.MIA_CHILD_ID;
52
+ const fetchImpl = opts.fetchImpl ?? fetch;
53
+ const now = opts.now ?? (() => Date.now());
54
+ // ── Step 1: Generate a fresh Ed25519 keypair ───────────────────────────────
55
+ const generated = generateKey(slug);
56
+ const { did, seedB64Url: senderSeed, pubXB64Url: publicKeyB64URL } = generated;
57
+ // ── Step 2: POST /api/v1/advisors/self-register ──────────────────────────────
58
+ // Canonical sandbox self-register route (#6 route split). The legacy
59
+ // /advisors/register shim still works for backward compat but is deprecated.
60
+ const regRes = await fetchImpl(`${censusUrl}/api/v1/advisors/self-register`, {
61
+ method: "POST",
62
+ headers: { "Content-Type": "application/json" },
63
+ body: JSON.stringify({
64
+ did,
65
+ public_key_b64url: publicKeyB64URL,
66
+ roles,
67
+ }),
68
+ });
69
+ if (!regRes.ok) {
70
+ const body = await regRes.text().catch(() => "");
71
+ throw new Error(`register: census returned HTTP ${regRes.status}: ${body}`);
72
+ }
73
+ const regBody = (await regRes.json());
74
+ if (regBody.trust_tier !== "provisional") {
75
+ throw new Error(`register: census returned unexpected tier ${regBody.trust_tier} (want provisional) — SECURITY VIOLATION`);
76
+ }
77
+ if (regBody.did !== did) {
78
+ throw new Error(`register: DID mismatch — sent ${did}, got ${regBody.did}`);
79
+ }
80
+ // ── Step 3: Verify DID appears on the trust-list immediately ──────────────
81
+ const tlRes = await fetchImpl(`${censusUrl}/.well-known/ocss/trust-list`);
82
+ if (!tlRes.ok) {
83
+ throw new Error(`register: trust-list fetch returned HTTP ${tlRes.status}`);
84
+ }
85
+ const signed = (await tlRes.json());
86
+ const doc = verifyDocument(signed, trustRootX);
87
+ const entry = doc.entries?.find((e) => e.did === did);
88
+ if (!entry) {
89
+ throw new Error(`register: DID ${did} not found on trust-list after registration — recompile did not fire (${doc.entries?.length ?? 0} entries)`);
90
+ }
91
+ if (entry.tier !== "provisional") {
92
+ throw new Error(`register: trust-list tier = ${entry.tier ?? "?"}, want provisional`);
93
+ }
94
+ // ── Step 4: Write .phosra.env ───────────────────────────────────────────────
95
+ // Written idempotently: overwrite if it exists (the new DID IS the new config).
96
+ const envContent = [
97
+ `# Generated by phosra register — ${new Date(now()).toISOString()}`,
98
+ `# This party's registered DID and signing seed. Keep PHOSRA_SENDER_SEED_B64URL secret.`,
99
+ `PHOSRA_DID=${did}`,
100
+ `PHOSRA_SENDER_SEED_B64URL=${senderSeed}`,
101
+ `PHOSRA_CENSUS_URL=${censusUrl}`,
102
+ `PHOSRA_TRUST_ROOT_X=${trustRootX}`,
103
+ ``,
104
+ ].join("\n");
105
+ writeFileSync(outFile, envContent, "utf-8");
106
+ // ── Step 5: Prove a green round-trip ───────────────────────────────────────
107
+ // household-acme is the CONSENT ISSUER (parent persona) — that is its sandbox role.
108
+ // The cold party (this DID) is the WRITER (writer_ref = this party's did).
109
+ const householdKeyId = HOUSEHOLD_ACME_KEY_ID;
110
+ const hhSenderKey = senderKeyFromSeedB64url(householdSeed, householdKeyId);
111
+ const coldSenderKey = senderKeyFromSeedB64url(senderSeed, generated.keyId);
112
+ const resolver = fromVerifiedDocument(doc, now);
113
+ let routerJwk;
114
+ try {
115
+ routerJwk = resolver.payloadKey(ROUTER_DID);
116
+ }
117
+ catch (e) {
118
+ throw new Error(`register round-trip: router payload key not found: ${String(e)}`);
119
+ }
120
+ // Step 5a: ingest a consent_attestation (signed by household-acme as consent issuer).
121
+ const idemSuffix = now().toString(36);
122
+ const consentIdemKey = `cli-register-rt-${idemSuffix}`;
123
+ const consentPayload = {
124
+ app_ref: ROUNDTRIP_APP_DID,
125
+ target: `child:${miaChildId}`,
126
+ band: "13_15",
127
+ consent_scope: "collection_parental_authority",
128
+ consent_method: `parental_consent_link:cli-register-${idemSuffix}`,
129
+ attested_at: new Date(now()).toISOString().replace(/\.\d{3}Z$/, "Z"),
130
+ expiry: "2030-01-01T00:00:00Z",
131
+ idempotency_key: consentIdemKey,
132
+ };
133
+ const transport = new CensusTransport({ baseUrl: censusUrl, senderKey: hhSenderKey, fetchImpl, now });
134
+ const consentEnvelope = await buildConsentAttestation(consentPayload, hhSenderKey, routerJwk, censusUrl, ROUTER_DID);
135
+ const consentRes = await transport.post("/api/v1/webhooks/inbound/app-store", consentEnvelope);
136
+ if (consentRes.status !== 201 && consentRes.status !== 200) {
137
+ const body = await consentRes.text().catch(() => "");
138
+ throw new Error(`register round-trip: consent ingest ${consentRes.status}: ${body}`);
139
+ }
140
+ // Step 5b: rule write signed by the COLD PARTY's key.
141
+ // writer_ref = cold party's DID (not household-acme); this proves the cold party
142
+ // can sign census requests now that it is on the trust-list.
143
+ const coldTransport = new CensusTransport({ baseUrl: censusUrl, senderKey: coldSenderKey, fetchImpl, now });
144
+ const ruleIdemKey = `cli-register-rule-${idemSuffix}`;
145
+ const coldDIDStr = did.split("#")[0]; // just the DID, no fragment
146
+ const ruleBody = {
147
+ rule_category: ROUNDTRIP_RULE_CATEGORY,
148
+ decision: "block",
149
+ target: { child_ref: `child:${miaChildId}` },
150
+ standing_ref: `consent:attestation:${consentIdemKey}`,
151
+ idempotency_key: ruleIdemKey,
152
+ writer_ref: coldDIDStr,
153
+ };
154
+ const ruleRes = await coldTransport.post(`/api/v1/policies/${miaPolicyId}/rules`, ruleBody);
155
+ if (ruleRes.status !== 201 && ruleRes.status !== 200) {
156
+ const body = await ruleRes.text().catch(() => "");
157
+ throw new Error(`register round-trip: rule write as ${coldDIDStr} returned HTTP ${ruleRes.status}: ${body}`);
158
+ }
159
+ return {
160
+ PHOSRA_DID: did,
161
+ PHOSRA_SENDER_SEED_B64URL: senderSeed,
162
+ PHOSRA_CENSUS_URL: censusUrl,
163
+ PHOSRA_TRUST_ROOT_X: trustRootX,
164
+ envFile: outFile,
165
+ };
166
+ }
167
+ //# sourceMappingURL=register.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"register.js","sourceRoot":"","sources":["../../src/commands/register.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,72 @@
1
+ /**
2
+ * Config loading for @phosra/cli.
3
+ *
4
+ * Priority: process.env > .phosra.env file > baked-in sandbox defaults.
5
+ * The sandbox defaults wire to the STAGING shared sandbox DID (household-acme,
6
+ * 16 seeded trust-list entries) — a partner goes green with ZERO new backend.
7
+ */
8
+ export declare const SANDBOX_DEFAULTS: {
9
+ readonly CENSUS_URL: "https://phosra-api-sandbox-staging.up.railway.app";
10
+ readonly TRUST_ROOT_X: "jDWZfB70DM8B3ZnPG8FQ0tnanqLgwJX4ZMr_E1Ugv3w";
11
+ readonly HOUSEHOLD_SEED: "aG91c2Vob2xkLWFjbWUBAQEBAQEBAQEBAQEBAQEBAQE";
12
+ readonly MIA_POLICY_ID: "b011c1e5-0000-4000-8000-000000000b01";
13
+ readonly MIA_CHILD_ID: "a11ce0fa-0000-4000-8000-0000000000a1";
14
+ };
15
+ /**
16
+ * The canonical keyID for the household-acme parent persona in the sandbox trust-list.
17
+ * Matches apps/sim-common/sandbox-seed.json entry "keyId": "did:ocss:household-acme#2026-06".
18
+ * NOTE: household-acme uses "#2026-06" (no "sandbox-" prefix) unlike jefferson-union/beacon.
19
+ */
20
+ export declare const HOUSEHOLD_ACME_KEY_ID = "did:ocss:household-acme#2026-06";
21
+ /** Router DID — the envelope intermediary for consent_attestation sealing. */
22
+ export declare const ROUTER_DID = "did:ocss:phosra-router";
23
+ /**
24
+ * app_ref for the doctor round-trip consent_attestation.
25
+ * Must be in the census's OCSS_CONSENT_ATTESTATION_APPS env on the staging sandbox.
26
+ * loopline is in the roster: "did:ocss:loopline,...".
27
+ */
28
+ export declare const ROUNDTRIP_APP_DID = "did:ocss:loopline";
29
+ /** Rule category written in the doctor round-trip. household-acme is accredited for this. */
30
+ export declare const ROUNDTRIP_RULE_CATEGORY = "addictive_pattern_block";
31
+ export interface PhosraConfig {
32
+ /** Base URL of the OCSS census (no trailing slash). */
33
+ censusUrl: string;
34
+ /** Base64url-raw Ed25519 root public key X — anchors all trust-list verification. */
35
+ trustRootX: string;
36
+ /** Base64url Ed25519 seed for the household-acme parent persona. */
37
+ householdSeed: string;
38
+ /** Optional: Postgres URL for @phosra/link grant store (link write only). */
39
+ databaseUrl?: string;
40
+ /** Optional: §9.3(b) endpoint_id_label for gatekeeper check. */
41
+ endpointId?: string;
42
+ /** When true, deterministic test-key derivation is allowed (sandbox only). */
43
+ testKeys: boolean;
44
+ /** Integration role — provider (parental-controls vendor) or platform. */
45
+ role?: "provider" | "platform";
46
+ /** Sandbox Mia policy ID for doctor probe. */
47
+ miaPolicyId: string;
48
+ /** Sandbox Mia child ID for doctor probe. */
49
+ miaChildId: string;
50
+ /**
51
+ * Optional: the base64url-raw Ed25519 seed for THIS party's own registered DID.
52
+ * Set by `phosra register` and read by `phosra doctor` to use the registered DID
53
+ * rather than the household-acme demo key.
54
+ * NOT the household-acme seed — that path is the zero-config demo only.
55
+ */
56
+ senderSeed?: string;
57
+ /**
58
+ * Optional: the registered DID for this party (e.g. "did:ocss:my-company").
59
+ * Set alongside senderSeed by `phosra register`.
60
+ */
61
+ did?: string;
62
+ }
63
+ /**
64
+ * Load the PhosraConfig from, in priority order:
65
+ * 1. process.env
66
+ * 2. .phosra.env in cwd (or opts.envFile)
67
+ * 3. baked-in sandbox defaults
68
+ */
69
+ export declare function loadConfig(opts?: {
70
+ envFile?: string;
71
+ }): PhosraConfig;
72
+ //# sourceMappingURL=config.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAQH,eAAO,MAAM,gBAAgB;;;;;;CAOnB,CAAC;AAEX;;;;GAIG;AACH,eAAO,MAAM,qBAAqB,oCAAoC,CAAC;AAEvE,8EAA8E;AAC9E,eAAO,MAAM,UAAU,2BAA2B,CAAC;AAEnD;;;;GAIG;AACH,eAAO,MAAM,iBAAiB,sBAAsB,CAAC;AAErD,6FAA6F;AAC7F,eAAO,MAAM,uBAAuB,4BAA4B,CAAC;AAEjE,MAAM,WAAW,YAAY;IAC3B,uDAAuD;IACvD,SAAS,EAAE,MAAM,CAAC;IAClB,qFAAqF;IACrF,UAAU,EAAE,MAAM,CAAC;IACnB,oEAAoE;IACpE,aAAa,EAAE,MAAM,CAAC;IACtB,6EAA6E;IAC7E,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,gEAAgE;IAChE,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,8EAA8E;IAC9E,QAAQ,EAAE,OAAO,CAAC;IAClB,0EAA0E;IAC1E,IAAI,CAAC,EAAE,UAAU,GAAG,UAAU,CAAC;IAC/B,8CAA8C;IAC9C,WAAW,EAAE,MAAM,CAAC;IACpB,6CAA6C;IAC7C,UAAU,EAAE,MAAM,CAAC;IACnB;;;;;OAKG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB;;;OAGG;IACH,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;AAiCD;;;;;GAKG;AACH,wBAAgB,UAAU,CAAC,IAAI,GAAE;IAAE,OAAO,CAAC,EAAE,MAAM,CAAA;CAAO,GAAG,YAAY,CAqBxE"}
package/dist/config.js ADDED
@@ -0,0 +1,94 @@
1
+ /**
2
+ * Config loading for @phosra/cli.
3
+ *
4
+ * Priority: process.env > .phosra.env file > baked-in sandbox defaults.
5
+ * The sandbox defaults wire to the STAGING shared sandbox DID (household-acme,
6
+ * 16 seeded trust-list entries) — a partner goes green with ZERO new backend.
7
+ */
8
+ import { readFileSync } from "node:fs";
9
+ import { join } from "node:path";
10
+ // Sandbox defaults — the STAGING shared sandbox census + household-acme DID.
11
+ // These baked-in values let `phosra init` and `phosra doctor` work out-of-the-box
12
+ // against the existing 16-entry trust-list without any provisioning.
13
+ export const SANDBOX_DEFAULTS = {
14
+ CENSUS_URL: "https://phosra-api-sandbox-staging.up.railway.app",
15
+ TRUST_ROOT_X: "jDWZfB70DM8B3ZnPG8FQ0tnanqLgwJX4ZMr_E1Ugv3w",
16
+ HOUSEHOLD_SEED: "aG91c2Vob2xkLWFjbWUBAQEBAQEBAQEBAQEBAQEBAQE",
17
+ // Sandbox Mia child/policy — seeded in the staging sandbox DB (doctor probe)
18
+ MIA_POLICY_ID: "b011c1e5-0000-4000-8000-000000000b01",
19
+ MIA_CHILD_ID: "a11ce0fa-0000-4000-8000-0000000000a1",
20
+ };
21
+ /**
22
+ * The canonical keyID for the household-acme parent persona in the sandbox trust-list.
23
+ * Matches apps/sim-common/sandbox-seed.json entry "keyId": "did:ocss:household-acme#2026-06".
24
+ * NOTE: household-acme uses "#2026-06" (no "sandbox-" prefix) unlike jefferson-union/beacon.
25
+ */
26
+ export const HOUSEHOLD_ACME_KEY_ID = "did:ocss:household-acme#2026-06";
27
+ /** Router DID — the envelope intermediary for consent_attestation sealing. */
28
+ export const ROUTER_DID = "did:ocss:phosra-router";
29
+ /**
30
+ * app_ref for the doctor round-trip consent_attestation.
31
+ * Must be in the census's OCSS_CONSENT_ATTESTATION_APPS env on the staging sandbox.
32
+ * loopline is in the roster: "did:ocss:loopline,...".
33
+ */
34
+ export const ROUNDTRIP_APP_DID = "did:ocss:loopline";
35
+ /** Rule category written in the doctor round-trip. household-acme is accredited for this. */
36
+ export const ROUNDTRIP_RULE_CATEGORY = "addictive_pattern_block";
37
+ /**
38
+ * Parse a .env-style key=value file.
39
+ * - Skips blank lines and # comments.
40
+ * - Strips optional surrounding single or double quotes from values.
41
+ * - Tolerates missing file (returns {}).
42
+ */
43
+ function parseDotenv(filePath) {
44
+ try {
45
+ const text = readFileSync(filePath, "utf-8");
46
+ const out = {};
47
+ for (const raw of text.split("\n")) {
48
+ const line = raw.trim();
49
+ if (!line || line.startsWith("#"))
50
+ continue;
51
+ const eq = line.indexOf("=");
52
+ if (eq < 0)
53
+ continue;
54
+ const key = line.slice(0, eq).trim();
55
+ let val = line.slice(eq + 1).trim();
56
+ if ((val.startsWith('"') && val.endsWith('"')) ||
57
+ (val.startsWith("'") && val.endsWith("'"))) {
58
+ val = val.slice(1, -1);
59
+ }
60
+ if (key)
61
+ out[key] = val;
62
+ }
63
+ return out;
64
+ }
65
+ catch {
66
+ return {};
67
+ }
68
+ }
69
+ /**
70
+ * Load the PhosraConfig from, in priority order:
71
+ * 1. process.env
72
+ * 2. .phosra.env in cwd (or opts.envFile)
73
+ * 3. baked-in sandbox defaults
74
+ */
75
+ export function loadConfig(opts = {}) {
76
+ const file = opts.envFile ?? join(process.cwd(), ".phosra.env");
77
+ const dot = parseDotenv(file);
78
+ const get = (key) => process.env[key] ?? dot[key];
79
+ return {
80
+ censusUrl: get("PHOSRA_CENSUS_URL") ?? SANDBOX_DEFAULTS.CENSUS_URL,
81
+ trustRootX: get("PHOSRA_TRUST_ROOT_X") ?? SANDBOX_DEFAULTS.TRUST_ROOT_X,
82
+ householdSeed: get("PHOSRA_HOUSEHOLD_SEED") ?? SANDBOX_DEFAULTS.HOUSEHOLD_SEED,
83
+ databaseUrl: get("PHOSRA_DATABASE_URL"),
84
+ endpointId: get("PHOSRA_ENDPOINT_ID"),
85
+ testKeys: (get("PHOSRA_TEST_KEYS") ?? "1") === "1",
86
+ role: get("PHOSRA_ROLE"),
87
+ miaPolicyId: get("SANDBOX_MIA_POLICY_ID") ?? SANDBOX_DEFAULTS.MIA_POLICY_ID,
88
+ miaChildId: get("SANDBOX_MIA_CHILD_ID") ?? SANDBOX_DEFAULTS.MIA_CHILD_ID,
89
+ // Phase 2a — registered DID + own seed (set by `phosra register`).
90
+ senderSeed: get("PHOSRA_SENDER_SEED_B64URL"),
91
+ did: get("PHOSRA_DID"),
92
+ };
93
+ }
94
+ //# sourceMappingURL=config.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Build a §4.2 consent_attestation OCSS envelope for the doctor round-trip.
3
+ *
4
+ * Seals the payload bytes to the router's EC P-256 payload JWK (JWE),
5
+ * wraps in an Outer, and signs the outer with the parent Ed25519 sender key.
6
+ * Mirrors @phosra/link census-client.ts sealConsentAttestation without pg.
7
+ *
8
+ * SANDBOX ONLY — called with household-acme (pre-admitted test key).
9
+ */
10
+ import { type Envelope, type SenderKey, type JWK } from "@openchildsafety/ocss";
11
+ export interface ConsentPayload {
12
+ app_ref: string;
13
+ target: string;
14
+ band: "under_13" | "13_15" | "16_17";
15
+ consent_scope: string;
16
+ consent_method: string;
17
+ attested_at: string;
18
+ expiry: string;
19
+ idempotency_key: string;
20
+ }
21
+ /**
22
+ * buildConsentAttestation seals the payload to the router and signs the outer
23
+ * with the parent persona key. Returns the wire-ready Envelope.
24
+ *
25
+ * @param payload Consent attestation fields (idempotency_key must be unique per run)
26
+ * @param parentKey household-acme SenderKey (signs the outer + HTTP request)
27
+ * @param routerJwk Router's EC P-256 payload public key (from the trust list)
28
+ * @param censusBaseUrl Census base URL — sets the envelope's `resource`
29
+ * @param routerDid Router DID — sets `inner.receiver` + `outer.intermediary_audience`
30
+ */
31
+ export declare function buildConsentAttestation(payload: ConsentPayload, parentKey: SenderKey, routerJwk: JWK, censusBaseUrl: string, routerDid: string): Promise<Envelope>;
32
+ //# sourceMappingURL=envelope.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"envelope.d.ts","sourceRoot":"","sources":["../src/envelope.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EAKL,KAAK,QAAQ,EAGb,KAAK,SAAS,EACd,KAAK,GAAG,EACT,MAAM,uBAAuB,CAAC;AAa/B,MAAM,WAAW,cAAc;IAC7B,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,UAAU,GAAG,OAAO,GAAG,OAAO,CAAC;IACrC,aAAa,EAAE,MAAM,CAAC;IACtB,cAAc,EAAE,MAAM,CAAC;IACvB,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,EAAE,MAAM,CAAC;IACf,eAAe,EAAE,MAAM,CAAC;CACzB;AAED;;;;;;;;;GASG;AACH,wBAAsB,uBAAuB,CAC3C,OAAO,EAAE,cAAc,EACvB,SAAS,EAAE,SAAS,EACpB,SAAS,EAAE,GAAG,EACd,aAAa,EAAE,MAAM,EACrB,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,QAAQ,CAAC,CAwBnB"}