@cotal-ai/auth 0.0.0 → 0.11.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 (55) hide show
  1. package/LICENSE +202 -0
  2. package/dist/callout.d.ts +108 -0
  3. package/dist/callout.d.ts.map +1 -0
  4. package/dist/callout.js +219 -0
  5. package/dist/callout.js.map +1 -0
  6. package/dist/commands.d.ts +2 -0
  7. package/dist/commands.d.ts.map +1 -0
  8. package/dist/commands.js +359 -0
  9. package/dist/commands.js.map +1 -0
  10. package/dist/derive.d.ts +15 -0
  11. package/dist/derive.d.ts.map +1 -0
  12. package/dist/derive.js +72 -0
  13. package/dist/derive.js.map +1 -0
  14. package/dist/idp.d.ts +63 -0
  15. package/dist/idp.d.ts.map +1 -0
  16. package/dist/idp.js +125 -0
  17. package/dist/idp.js.map +1 -0
  18. package/dist/index.d.ts +13 -0
  19. package/dist/index.d.ts.map +1 -0
  20. package/dist/index.js +13 -0
  21. package/dist/index.js.map +1 -0
  22. package/dist/issuer.d.ts +78 -0
  23. package/dist/issuer.d.ts.map +1 -0
  24. package/dist/issuer.js +137 -0
  25. package/dist/issuer.js.map +1 -0
  26. package/dist/ledger.d.ts +108 -0
  27. package/dist/ledger.d.ts.map +1 -0
  28. package/dist/ledger.js +399 -0
  29. package/dist/ledger.js.map +1 -0
  30. package/dist/login.d.ts +69 -0
  31. package/dist/login.d.ts.map +1 -0
  32. package/dist/login.js +338 -0
  33. package/dist/login.js.map +1 -0
  34. package/dist/permissions.d.ts +28 -0
  35. package/dist/permissions.d.ts.map +1 -0
  36. package/dist/permissions.js +50 -0
  37. package/dist/permissions.js.map +1 -0
  38. package/dist/provider.d.ts +18 -0
  39. package/dist/provider.d.ts.map +1 -0
  40. package/dist/provider.js +213 -0
  41. package/dist/provider.js.map +1 -0
  42. package/dist/service.d.ts +10 -0
  43. package/dist/service.d.ts.map +1 -0
  44. package/dist/service.js +288 -0
  45. package/dist/service.js.map +1 -0
  46. package/dist/store.d.ts +82 -0
  47. package/dist/store.d.ts.map +1 -0
  48. package/dist/store.js +208 -0
  49. package/dist/store.js.map +1 -0
  50. package/dist/token.d.ts +68 -0
  51. package/dist/token.d.ts.map +1 -0
  52. package/dist/token.js +128 -0
  53. package/dist/token.js.map +1 -0
  54. package/package.json +35 -5
  55. package/README.md +0 -4
@@ -0,0 +1 @@
1
+ {"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../src/store.ts"],"names":[],"mappings":"AAyBA,OAAO,EAAqB,KAAK,WAAW,EAAE,KAAK,qBAAqB,EAAE,MAAM,cAAc,CAAC;AAE/F,OAAO,EAML,KAAK,eAAe,EACrB,MAAM,aAAa,CAAC;AAOrB;;kGAEkG;AAClG,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAGjD;AAwBD,uGAAuG;AACvG,wBAAgB,eAAe,CAAC,GAAG,EAAE,MAAM,GAAG,WAAW,GAAG,SAAS,CAOpE;AAED;;+DAE+D;AAC/D,wBAAsB,iBAAiB,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,qBAAqB,GAAG,OAAO,CAAC,WAAW,CAAC,CAOvG;AAWD;6CAC6C;AAC7C,wBAAsB,UAAU,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,eAAe,GAAG,SAAS,CAAC,CAclF;AAED;;;kGAGkG;AAClG,wBAAsB,YAAY,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,eAAe,CAAC,CAevF;AAUD,4FAA4F;AAC5F,wBAAgB,eAAe,CAAC,GAAG,EAAE,MAAM,GAAG,UAAU,GAAG,SAAS,CAOnE;AAED;sFACsF;AACtF,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,MAAM,GAAG,UAAU,CAUzD;AAMD;;;;2EAI2E;AAC3E,MAAM,WAAW,SAAS;IACxB,+EAA+E;IAC/E,GAAG,EAAE,MAAM,CAAC;IACZ,iCAAiC;IACjC,MAAM,EAAE,MAAM,CAAC;IACf,iCAAiC;IACjC,QAAQ,EAAE,MAAM,CAAC;IACjB,iEAAiE;IACjE,OAAO,EAAE,MAAM,CAAC;CACjB;AAMD,wEAAwE;AACxE,wBAAgB,aAAa,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,GAAG,SAAS,CAMhE;AAED;;;6BAG6B;AAC7B,wBAAgB,eAAe,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,SAAS,CAoBvE;AAMD;;;;iCAIiC;AACjC,MAAM,WAAW,WAAW;IAC1B,WAAW,EAAE;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,WAAW,EAAE,MAAM,CAAA;KAAE,CAAC;CACnD;AAMD,wBAAgB,eAAe,CAAC,GAAG,EAAE,MAAM,GAAG,WAAW,GAAG,SAAS,CAMpE;AAED;;+CAE+C;AAC/C,wBAAgB,eAAe,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,WAAW,GAAG,IAAI,CAGpE;AAMD;;;;;kFAKkF;AAClF,MAAM,WAAW,eAAe;IAC9B,uEAAuE;IACvE,GAAG,EAAE,MAAM,CAAC;IACZ,GAAG,EAAE,MAAM,CAAC;IACZ,6FAA6F;IAC7F,GAAG,EAAE,MAAM,CAAC;CACb;AAMD,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,MAAM,GAAG,eAAe,GAAG,SAAS,CAM5E;AAED,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,eAAe,GAAG,IAAI,CAG5E;AAED;mDACmD;AACnD,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAEtD"}
package/dist/store.js ADDED
@@ -0,0 +1,208 @@
1
+ /**
2
+ * On-disk persistence for a space's USER-AUTH material, under the same `.cotal/auth/` dir as the
3
+ * space trust material (explicit-dir APIs, the auth-paths posture: the caller picks the dir, nothing
4
+ * here discovers paths ambiently). Everything is generate-on-first-use and STABLE thereafter — the
5
+ * callout account identity, the issuer signing keys, and the owner-derivation secret must survive
6
+ * restarts, or previously-minted credentials/owners silently break:
7
+ *
8
+ * - `callout.json` — the dedicated auth-callout account ({@link CalloutAuth}: account + signing key,
9
+ * callout service creds, deny-all sentinel creds, xkey). Regenerating it would orphan the account
10
+ * the broker config preloads; minting it the first time needs the FULL SpaceAuth (operator seed).
11
+ * - `issuer.json` — the Cotal user-bearer issuer: its pinned `iss` string plus the serialized
12
+ * Ed25519 signing keys (private JWKs) and the active kid. Losing these kills every outstanding
13
+ * bearer (rotation is `rotate`/`retire` on the loaded issuer, then {@link saveIssuerFile}).
14
+ * - `owner-secret.json` — the per-space owner-derivation secret (32 random bytes). REGENERATING IT
15
+ * RE-KEYS EVERY OWNER in the space (same human → different `u_…`), which is a migration, never an
16
+ * accident — hence load-or-create with no overwrite path.
17
+ *
18
+ * All files 0600 under a 0700 dir (core's secret-fs helpers), atomic writes, versioned JSON with
19
+ * fail-loud parsing (the login session-cache posture: a torn/hand-edited credential file surfaces as
20
+ * one legible sentence, never a raw SyntaxError downstream).
21
+ */
22
+ import { existsSync, readFileSync, rmSync } from "node:fs";
23
+ import { randomBytes } from "node:crypto";
24
+ import { join } from "node:path";
25
+ import { mkSecretDir, writeSecretFileAtomic } from "@cotal-ai/core";
26
+ import { createCalloutAuth } from "./callout.js";
27
+ import { normalizeIdpUrl } from "./login.js";
28
+ import { createUserTokenIssuer, exportSigningKey, generateSigningKey, importSigningKey, } from "./issuer.js";
29
+ const CALLOUT_FILE = "callout.json";
30
+ const ISSUER_FILE = "issuer.json";
31
+ const OWNER_SECRET_FILE = "owner-secret.json";
32
+ const STORE_VER = 1;
33
+ /** The pinned `iss` for a space's Cotal user bearers — a stable URN, deliberately NOT the auth
34
+ * service's URL (the service port is ephemeral; an issuer pin must never change across restarts).
35
+ * Both the issuer (mint) and the callout (validate) take it from here — one source, no drift. */
36
+ export function spaceIssuer(space) {
37
+ if (!space)
38
+ throw new Error("spaceIssuer: a space is required");
39
+ return `urn:cotal:auth:${space}`;
40
+ }
41
+ /** Fail-loud versioned-JSON read: a missing file is undefined; a torn/edited/unknown-version file is
42
+ * a legible sentence naming the file and the recovery, never a raw parse error downstream. */
43
+ function readStoreFile(path, what) {
44
+ if (!existsSync(path))
45
+ return undefined;
46
+ let parsed;
47
+ try {
48
+ parsed = JSON.parse(readFileSync(path, "utf8"));
49
+ }
50
+ catch (e) {
51
+ throw new Error(`${path}: the ${what} file is not valid JSON (${e instanceof Error ? e.message : String(e)}) - restore it from backup; regenerating breaks existing credentials`);
52
+ }
53
+ if (parsed === null || typeof parsed !== "object" || parsed.ver !== STORE_VER)
54
+ throw new Error(`${path}: unknown ${what} version ${String(parsed?.ver)} (expected ${STORE_VER}) - refusing to guess at credential material`);
55
+ return parsed;
56
+ }
57
+ /** Load the persisted callout account material, or undefined if this space never enabled user auth. */
58
+ export function loadCalloutAuth(dir) {
59
+ const f = readStoreFile(join(dir, CALLOUT_FILE), "callout account");
60
+ if (!f)
61
+ return undefined;
62
+ const c = f.callout;
63
+ if (!c?.account?.pub || !c.account.jwt || !c.calloutCreds || !c.sentinelCreds || !c.xkey?.seed)
64
+ throw new Error(`${join(dir, CALLOUT_FILE)}: malformed callout account material - restore it from backup; regenerating orphans the preloaded auth account`);
65
+ return c;
66
+ }
67
+ /** Load-or-create the callout account: first call on a space mints it (operator seed required)
68
+ * and persists; every later call returns the SAME account, so the broker config preload and
69
+ * previously-issued sentinel creds stay valid. Idempotent. */
70
+ export async function ensureCalloutAuth(dir, input) {
71
+ const existing = loadCalloutAuth(dir);
72
+ if (existing)
73
+ return existing;
74
+ const callout = await createCalloutAuth(input);
75
+ mkSecretDir(dir); // harden BEFORE the secret lands
76
+ writeSecretFileAtomic(join(dir, CALLOUT_FILE), JSON.stringify({ ver: STORE_VER, callout }, null, 2));
77
+ return callout;
78
+ }
79
+ /** Load the persisted issuer (all published kids live; the persisted active kid signs), or undefined
80
+ * if this space never enabled user auth. */
81
+ export async function loadIssuer(dir) {
82
+ const f = readStoreFile(join(dir, ISSUER_FILE), "issuer key");
83
+ if (!f)
84
+ return undefined;
85
+ if (!f.issuer || !Array.isArray(f.keys) || !f.keys.length || !f.activeKid)
86
+ throw new Error(`${join(dir, ISSUER_FILE)}: malformed issuer key file - restore it from backup; regenerating kills every outstanding bearer`);
87
+ const active = f.keys.find((k) => k.kid === f.activeKid);
88
+ if (!active)
89
+ throw new Error(`${join(dir, ISSUER_FILE)}: active kid ${f.activeKid} is not in the key set - restore it from backup`);
90
+ // createUserTokenIssuer starts with one active key; rotate() adds the rest (each rotate re-points
91
+ // active, so feed the non-active keys THROUGH rotate and finish on the persisted active kid).
92
+ const issuer = createUserTokenIssuer({ issuer: f.issuer, key: await importSigningKey(active) });
93
+ for (const k of f.keys)
94
+ if (k.kid !== f.activeKid)
95
+ issuer.rotate(await importSigningKey(k));
96
+ if (issuer.activeKid() !== f.activeKid)
97
+ issuer.rotate(await importSigningKey(active));
98
+ return issuer;
99
+ }
100
+ /** Load-or-create the space's user-bearer issuer: first call generates the Ed25519 signing key and
101
+ * persists it under the pinned {@link spaceIssuer} `iss`; later calls return the SAME keys, so
102
+ * outstanding bearers keep verifying. A persisted `iss` that disagrees with the space's pin fails
103
+ * loud (the material belongs to another space/layout — never sign under a mismatched issuer). */
104
+ export async function ensureIssuer(dir, space) {
105
+ const iss = spaceIssuer(space);
106
+ const existing = await loadIssuer(dir);
107
+ if (existing) {
108
+ if (existing.issuer !== iss)
109
+ throw new Error(`${join(dir, ISSUER_FILE)}: persisted issuer "${existing.issuer}" != this space's pin "${iss}" - the material belongs to a different space; refusing to mint under it`);
110
+ return existing;
111
+ }
112
+ const key = await generateSigningKey();
113
+ mkSecretDir(dir);
114
+ writeSecretFileAtomic(join(dir, ISSUER_FILE), JSON.stringify({ ver: STORE_VER, issuer: iss, activeKid: key.kid, keys: [await exportSigningKey(key)] }, null, 2));
115
+ return createUserTokenIssuer({ issuer: iss, key });
116
+ }
117
+ /** Load the owner-derivation secret, or undefined if this space never enabled user auth. */
118
+ export function loadOwnerSecret(dir) {
119
+ const f = readStoreFile(join(dir, OWNER_SECRET_FILE), "owner secret");
120
+ if (!f)
121
+ return undefined;
122
+ const secret = Buffer.from(f.secretB64 ?? "", "base64");
123
+ if (secret.byteLength !== 32)
124
+ throw new Error(`${join(dir, OWNER_SECRET_FILE)}: malformed owner secret - restore it from backup; a regenerated secret RE-KEYS EVERY OWNER in the space`);
125
+ return new Uint8Array(secret);
126
+ }
127
+ /** Load-or-create the per-space owner-derivation secret. There is deliberately NO regenerate path:
128
+ * a new secret re-keys every owner in the space (a migration, never an accident). */
129
+ export function ensureOwnerSecret(dir) {
130
+ const existing = loadOwnerSecret(dir);
131
+ if (existing)
132
+ return existing;
133
+ const secret = randomBytes(32);
134
+ mkSecretDir(dir);
135
+ writeSecretFileAtomic(join(dir, OWNER_SECRET_FILE), JSON.stringify({ ver: STORE_VER, secretB64: Buffer.from(secret).toString("base64") }, null, 2));
136
+ return new Uint8Array(secret);
137
+ }
138
+ // ---- the pinned external IdP ----
139
+ const IDP_FILE = "idp.json";
140
+ /** Load the pinned IdP, or undefined if this space never pinned one. */
141
+ export function loadPinnedIdp(dir) {
142
+ const f = readStoreFile(join(dir, IDP_FILE), "IdP pin");
143
+ if (!f)
144
+ return undefined;
145
+ if (!f.url || !f.issuer || !f.audience || !f.jwksUri)
146
+ throw new Error(`${join(dir, IDP_FILE)}: malformed IdP pin - re-pin with \`cotal up --user-auth --idp <url>\``);
147
+ return { url: f.url, issuer: f.issuer, audience: f.audience, jwksUri: f.jwksUri };
148
+ }
149
+ /** Pin-or-verify the space's IdP. First call REQUIRES a URL (there is no default IdP); later calls
150
+ * either omit it (reuse the pin) or must MATCH it — re-pointing a space at a different IdP re-keys
151
+ * every derived owner (the issuer is part of the derivation input), a migration this refuses to do
152
+ * as a flag side-effect. */
153
+ export function ensurePinnedIdp(dir, idpUrl) {
154
+ const existing = loadPinnedIdp(dir);
155
+ if (existing) {
156
+ if (idpUrl !== undefined && idpUrl !== existing.url)
157
+ throw new Error(`this space's IdP is pinned to ${existing.url}; --idp ${idpUrl} would re-key every derived owner. ` +
158
+ `Re-pointing an IdP is a migration - remove ${join(dir, IDP_FILE)} deliberately if you mean it.`);
159
+ return existing;
160
+ }
161
+ if (!idpUrl)
162
+ throw new Error("user auth needs an IdP on first enable - run with --idp <auth base URL> (Better Auth: <origin>/api/auth)");
163
+ // Same normalization + scheme guard as `cotal login` (https, or loopback http for dev; no
164
+ // query/hash/userinfo) — the pin and the login cache must key on the SAME canonical URL.
165
+ const url = normalizeIdpUrl(idpUrl);
166
+ const origin = new URL(url).origin;
167
+ const pin = { url, issuer: origin, audience: origin, jwksUri: `${url}/jwks` };
168
+ mkSecretDir(dir);
169
+ writeSecretFileAtomic(join(dir, IDP_FILE), JSON.stringify({ ver: STORE_VER, ...pin }, null, 2));
170
+ return pin;
171
+ }
172
+ // ---- the auth service's own key projection ----
173
+ const SERVICE_KEYS_FILE = "service-keys.json";
174
+ export function loadServiceKeys(dir) {
175
+ const f = readStoreFile(join(dir, SERVICE_KEYS_FILE), "service key projection");
176
+ if (!f)
177
+ return undefined;
178
+ if (!f.dataAccount?.pub || !f.dataAccount.signingSeed)
179
+ throw new Error(`${join(dir, SERVICE_KEYS_FILE)}: malformed service key projection - re-run \`cotal up --user-auth\` to rewrite it`);
180
+ return { dataAccount: { pub: f.dataAccount.pub, signingSeed: f.dataAccount.signingSeed } };
181
+ }
182
+ /** Write (or refresh) the service's key projection. Idempotent overwrite — the projection carries
183
+ * no identity of its own, it IS the (stable) data-account signing material, so rewriting from the
184
+ * same space bundle is a no-op in content. */
185
+ export function saveServiceKeys(dir, keys) {
186
+ mkSecretDir(dir);
187
+ writeSecretFileAtomic(join(dir, SERVICE_KEYS_FILE), JSON.stringify({ ver: STORE_VER, ...keys }, null, 2));
188
+ }
189
+ // ---- the auth-service runtime discovery file ----
190
+ const SERVICE_FILE = "auth-service.json";
191
+ export function loadAuthServiceInfo(dir) {
192
+ const f = readStoreFile(join(dir, SERVICE_FILE), "auth service info");
193
+ if (!f)
194
+ return undefined;
195
+ if (!f.url || typeof f.pid !== "number" || !f.cap)
196
+ throw new Error(`${join(dir, SERVICE_FILE)}: malformed auth service info - restart the mesh (\`cotal down\` then \`cotal up\`)`);
197
+ return { url: f.url, pid: f.pid, cap: f.cap };
198
+ }
199
+ export function saveAuthServiceInfo(dir, info) {
200
+ mkSecretDir(dir);
201
+ writeSecretFileAtomic(join(dir, SERVICE_FILE), JSON.stringify({ ver: STORE_VER, ...info }, null, 2));
202
+ }
203
+ /** Remove the discovery file (daemon shutdown / CLI pre-start scrub) so a stale entry can never
204
+ * satisfy a readiness poll for the NEXT start. */
205
+ export function clearAuthServiceInfo(dir) {
206
+ rmSync(join(dir, SERVICE_FILE), { force: true });
207
+ }
208
+ //# sourceMappingURL=store.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"store.js","sourceRoot":"","sources":["../src/store.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,68 @@
1
+ import type { CryptoKey, JWTVerifyGetKey } from "jose";
2
+ /** Current normative token-shape version. Bump only with a SPEC change; validators reject
3
+ * anything else (older = downgrade, newer = from-the-future misconfig). */
4
+ export declare const USER_TOKEN_VER = 1;
5
+ /** Default cap on `exp - iat`. 15 minutes: long enough for connect + retries, short enough that
6
+ * re-mint is the working revocation path. */
7
+ export declare const MAX_TOKEN_TTL_SEC = 900;
8
+ /** The elevated per-connection profiles a bearer may request at exchange time (the "views"):
9
+ * read-only god view, space-history purge, channel delete, channel-registry writes, and the
10
+ * manifest-deploy preflight. Server-authored into `act.view` ONLY by the human exchange after a
11
+ * fresh ledger check against {@link VIEW_REQUIRED_SCOPE}; the callout mints the matching profile
12
+ * instead of `agent`. A closed enum on BOTH the mint and validate side — an unknown view fails
13
+ * closed, never falls back to a profile. Deliberately NOT a generic `view=<profile>` passthrough:
14
+ * most profiles are daemon/provisioning surfaces that must never become human-requestable. */
15
+ export declare const USER_TOKEN_VIEWS: readonly ["admin", "purger", "channel-purger", "channel-writer", "deployer"];
16
+ export type UserTokenView = (typeof USER_TOKEN_VIEWS)[number];
17
+ /** The ONE central view policy table: which ledger capability each view's exchange requires (and
18
+ * the callout re-asserts, defense in depth). `admin` = operator authority (god-view read +
19
+ * destructive space writes); `deployer` is spawn-grade — deploying YOUR OWN team's manifest rides
20
+ * the same owner-domain model as own-agent stop/attach (the manager still enforces owner equality
21
+ * at launch, and the view's control grant is the PRIVILEGED tier, never the admin bypass). */
22
+ export declare const VIEW_REQUIRED_SCOPE: Record<UserTokenView, "admin" | "spawn">;
23
+ /** The server-authored actor claim. `owner` restates `sub` (cross-checked); `actor` is the
24
+ * ledger-derived agent-instance id; `parent` is at most ONE spawner audit link; `view` is the
25
+ * exchange-authorized elevated profile request (absent = the agent profile). */
26
+ export interface UserTokenActor {
27
+ owner: string;
28
+ actor: string;
29
+ scope?: string[];
30
+ /** The spawning principal (`<owner>.<actor>` dot-form), when this agent was spawned by another. */
31
+ parent?: string;
32
+ /** Exchange-authorized elevated view ({@link USER_TOKEN_VIEWS}); absent = agent profile. */
33
+ view?: UserTokenView;
34
+ }
35
+ /** A fully validated user token, reduced to what the callout needs. */
36
+ export interface ValidatedUserToken {
37
+ /** The opaque derived owner (== `sub`, format-asserted). */
38
+ owner: string;
39
+ /** The space this token is scoped to (== `aud`). */
40
+ space: string;
41
+ /** Capability scope (`scope` claim; empty when absent). */
42
+ scope: string[];
43
+ /** The validated actor claim. */
44
+ act: UserTokenActor;
45
+ /** Token-shape version (== {@link USER_TOKEN_VER}). */
46
+ ver: number;
47
+ /** Bearer expiry (unix seconds) — the callout binds the minted NATS user JWT's lifetime to it,
48
+ * so broker access dies with the bearer (the v1 revocation lever, enforced server-side). */
49
+ exp: number;
50
+ }
51
+ export interface ValidateUserTokenOpts {
52
+ /** The pinned verification key path: a `createRemoteJWKSet(new URL(<pinned origin>))` resolver
53
+ * or a local public key. The token itself NEVER influences where the key comes from. */
54
+ key: JWTVerifyGetKey | CryptoKey;
55
+ /** Exact expected issuer (the IdP bridge). */
56
+ issuer: string;
57
+ /** Exact expected audience — the space name. */
58
+ audience: string;
59
+ /** Override of the {@link MAX_TOKEN_TTL_SEC} lifetime cap (tests only; keep short). */
60
+ maxTtlSec?: number;
61
+ /** Clock skew tolerance in seconds (default 5). */
62
+ clockToleranceSec?: number;
63
+ }
64
+ /** Validate a user bearer token. Returns the reduced, validated claims or THROWS — there is no
65
+ * partially-valid result and no fallback (a validation failure at the callout is a denied
66
+ * connection). */
67
+ export declare function validateUserToken(token: string, opts: ValidateUserTokenOpts): Promise<ValidatedUserToken>;
68
+ //# sourceMappingURL=token.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"token.d.ts","sourceRoot":"","sources":["../src/token.ts"],"names":[],"mappings":"AAoBA,OAAO,KAAK,EAAE,SAAS,EAAE,eAAe,EAAE,MAAM,MAAM,CAAC;AAGvD;4EAC4E;AAC5E,eAAO,MAAM,cAAc,IAAI,CAAC;AAEhC;8CAC8C;AAC9C,eAAO,MAAM,iBAAiB,MAAM,CAAC;AAErC;;;;;;+FAM+F;AAC/F,eAAO,MAAM,gBAAgB,8EAA+E,CAAC;AAC7G,MAAM,MAAM,aAAa,GAAG,CAAC,OAAO,gBAAgB,CAAC,CAAC,MAAM,CAAC,CAAC;AAE9D;;;;+FAI+F;AAC/F,eAAO,MAAM,mBAAmB,EAAE,MAAM,CAAC,aAAa,EAAE,OAAO,GAAG,OAAO,CAMxE,CAAC;AAEF;;iFAEiF;AACjF,MAAM,WAAW,cAAc;IAC7B,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC;IACjB,mGAAmG;IACnG,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,4FAA4F;IAC5F,IAAI,CAAC,EAAE,aAAa,CAAC;CACtB;AAED,uEAAuE;AACvE,MAAM,WAAW,kBAAkB;IACjC,4DAA4D;IAC5D,KAAK,EAAE,MAAM,CAAC;IACd,oDAAoD;IACpD,KAAK,EAAE,MAAM,CAAC;IACd,2DAA2D;IAC3D,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,iCAAiC;IACjC,GAAG,EAAE,cAAc,CAAC;IACpB,uDAAuD;IACvD,GAAG,EAAE,MAAM,CAAC;IACZ;iGAC6F;IAC7F,GAAG,EAAE,MAAM,CAAC;CACb;AAED,MAAM,WAAW,qBAAqB;IACpC;6FACyF;IACzF,GAAG,EAAE,eAAe,GAAG,SAAS,CAAC;IACjC,8CAA8C;IAC9C,MAAM,EAAE,MAAM,CAAC;IACf,gDAAgD;IAChD,QAAQ,EAAE,MAAM,CAAC;IACjB,uFAAuF;IACvF,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,mDAAmD;IACnD,iBAAiB,CAAC,EAAE,MAAM,CAAC;CAC5B;AAED;;mBAEmB;AACnB,wBAAsB,iBAAiB,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,qBAAqB,GAAG,OAAO,CAAC,kBAAkB,CAAC,CA4E/G"}
package/dist/token.js ADDED
@@ -0,0 +1,128 @@
1
+ /**
2
+ * User-token validation — the strict Plane-2 check the auth callout runs on the bearer an agent
3
+ * presents in `auth_token` (SPEC-normative claim shape; the plan's §"Token shape").
4
+ *
5
+ * Claim shape (normative): `sub` = the opaque derived owner · `act` = server-authored
6
+ * { owner, actor, scope?, parent? } with AT MOST one parent/spawner audit link (nested delegation
7
+ * chains are cross-account scope, not this plan) · `aud` = the space · `scope` = capability list ·
8
+ * `ver` = token-shape version (a stale `ver` is rejected — downgrade defense).
9
+ *
10
+ * Validation is fail-closed and pinned end-to-end:
11
+ * - `alg` pinned to EdDSA (Ed25519) — nothing else verifies;
12
+ * - key-smuggling headers (`jku` / `jwk` / `x5u` / `x5c`) are rejected outright — the key is
13
+ * resolved ONLY through the caller-pinned resolver (a `createRemoteJWKSet` on a pinned origin,
14
+ * or a local key); a `kid` never resolves anywhere else;
15
+ * - `iss` / `aud` enforced; `exp`, `iat`, and `nbf` are REQUIRED; the lifetime is capped and
16
+ * ANCHORED TO THE VALIDATOR'S CLOCK — future-dated `iat`/`exp` windows are rejected, not just
17
+ * long spans (short-lived tokens are the v1 revocation lever — an unbounded or post-dated
18
+ * token would quietly disable it).
19
+ */
20
+ import { decodeProtectedHeader, jwtVerify } from "jose";
21
+ import { assertDerivedOwnerToken, assertValidOwnerToken } from "@cotal-ai/core";
22
+ /** Current normative token-shape version. Bump only with a SPEC change; validators reject
23
+ * anything else (older = downgrade, newer = from-the-future misconfig). */
24
+ export const USER_TOKEN_VER = 1;
25
+ /** Default cap on `exp - iat`. 15 minutes: long enough for connect + retries, short enough that
26
+ * re-mint is the working revocation path. */
27
+ export const MAX_TOKEN_TTL_SEC = 900;
28
+ /** The elevated per-connection profiles a bearer may request at exchange time (the "views"):
29
+ * read-only god view, space-history purge, channel delete, channel-registry writes, and the
30
+ * manifest-deploy preflight. Server-authored into `act.view` ONLY by the human exchange after a
31
+ * fresh ledger check against {@link VIEW_REQUIRED_SCOPE}; the callout mints the matching profile
32
+ * instead of `agent`. A closed enum on BOTH the mint and validate side — an unknown view fails
33
+ * closed, never falls back to a profile. Deliberately NOT a generic `view=<profile>` passthrough:
34
+ * most profiles are daemon/provisioning surfaces that must never become human-requestable. */
35
+ export const USER_TOKEN_VIEWS = ["admin", "purger", "channel-purger", "channel-writer", "deployer"];
36
+ /** The ONE central view policy table: which ledger capability each view's exchange requires (and
37
+ * the callout re-asserts, defense in depth). `admin` = operator authority (god-view read +
38
+ * destructive space writes); `deployer` is spawn-grade — deploying YOUR OWN team's manifest rides
39
+ * the same owner-domain model as own-agent stop/attach (the manager still enforces owner equality
40
+ * at launch, and the view's control grant is the PRIVILEGED tier, never the admin bypass). */
41
+ export const VIEW_REQUIRED_SCOPE = {
42
+ admin: "admin",
43
+ purger: "admin",
44
+ "channel-purger": "admin",
45
+ "channel-writer": "admin",
46
+ deployer: "spawn",
47
+ };
48
+ /** Validate a user bearer token. Returns the reduced, validated claims or THROWS — there is no
49
+ * partially-valid result and no fallback (a validation failure at the callout is a denied
50
+ * connection). */
51
+ export async function validateUserToken(token, opts) {
52
+ const header = decodeProtectedHeader(token);
53
+ if (header.jku !== undefined || header.jwk !== undefined || header.x5u !== undefined || header.x5c !== undefined)
54
+ throw new Error("user token: embedded key material (jku/jwk/x5u/x5c) is rejected - keys resolve only via the pinned JWKS");
55
+ if (header.alg !== "EdDSA")
56
+ throw new Error(`user token: alg must be EdDSA (got ${String(header.alg)})`);
57
+ const { payload } = await jwtVerify(token, opts.key, {
58
+ algorithms: ["EdDSA"],
59
+ issuer: opts.issuer,
60
+ audience: opts.audience,
61
+ clockTolerance: opts.clockToleranceSec ?? 5,
62
+ });
63
+ // jose's `audience` option is SET-MEMBERSHIP (an aud array containing the space passes) — the
64
+ // bearer must be scoped to EXACTLY this space: the plain string, or a singleton array of it. A
65
+ // multi-audience bearer would be replayable across spaces.
66
+ if (payload.aud !== opts.audience && !(Array.isArray(payload.aud) && payload.aud.length === 1 && payload.aud[0] === opts.audience))
67
+ throw new Error("user token: aud must be exactly the space - a multi-audience bearer is rejected");
68
+ if (typeof payload.exp !== "number")
69
+ throw new Error("user token: exp is required (tokens must be short-lived)");
70
+ if (typeof payload.iat !== "number")
71
+ throw new Error("user token: iat is required");
72
+ if (typeof payload.nbf !== "number")
73
+ throw new Error("user token: nbf is required");
74
+ // The lifetime cap must hold from the VALIDATOR's clock, not the token's own claims — a signed
75
+ // token post-dated a year out (iat/exp both in the future) satisfies `exp - iat ≤ cap` and
76
+ // sails through exp/nbf checks, giving it an effective validity far beyond the cap and quietly
77
+ // defeating the short-lived-token revocation lever. So: iat may not be in the future, and exp
78
+ // may not sit further than the cap from now.
79
+ const maxTtl = opts.maxTtlSec ?? MAX_TOKEN_TTL_SEC;
80
+ const tol = opts.clockToleranceSec ?? 5;
81
+ const now = Math.floor(Date.now() / 1000);
82
+ if (payload.iat > now + tol)
83
+ throw new Error("user token: iat is in the future");
84
+ if (payload.exp > now + maxTtl + tol)
85
+ throw new Error(`user token: exp is more than the ${maxTtl}s cap past now - short-lived tokens are the revocation lever`);
86
+ if (payload.exp - payload.iat > maxTtl)
87
+ throw new Error(`user token: lifetime ${payload.exp - payload.iat}s exceeds the ${maxTtl}s cap - short-lived tokens are the revocation lever`);
88
+ if (payload.ver !== USER_TOKEN_VER)
89
+ throw new Error(`user token: ver ${String(payload.ver)} != ${USER_TOKEN_VER} - stale or unknown token shape (downgrade defense)`);
90
+ if (typeof payload.sub !== "string")
91
+ throw new Error("user token: sub must be a string token - no coercion at a trust boundary");
92
+ const owner = assertDerivedOwnerToken(payload.sub);
93
+ const act = payload.act;
94
+ if (!act || typeof act !== "object")
95
+ throw new Error("user token: act claim is required (server-authored owner/actor)");
96
+ if (act.owner !== owner)
97
+ throw new Error(`user token: act.owner "${String(act.owner)}" != sub "${owner}" - inconsistent principal`);
98
+ if (typeof act.actor !== "string")
99
+ throw new Error("user token: act.actor must be a string token - no coercion at a trust boundary");
100
+ assertValidOwnerToken(act.actor);
101
+ if (act.scope !== undefined && !(Array.isArray(act.scope) && act.scope.every((s) => typeof s === "string")))
102
+ throw new Error("user token: act.scope must be a string list when present");
103
+ if (act.parent !== undefined) {
104
+ // At most ONE parent audit link, and it must BE a principal (dot-form <owner>.<actor> with a
105
+ // derived owner) — an arbitrary string here would flow into mint/audit trails unchecked.
106
+ if (typeof act.parent !== "string")
107
+ throw new Error("user token: act.parent must be a single principal string when present (no delegation chains)");
108
+ const parts = act.parent.split(".");
109
+ if (parts.length !== 2)
110
+ throw new Error(`user token: act.parent "${act.parent}" is not a principal (<owner>.<actor> dot-form)`);
111
+ assertDerivedOwnerToken(parts[0]);
112
+ assertValidOwnerToken(parts[1]);
113
+ }
114
+ if (act.view !== undefined && !USER_TOKEN_VIEWS.includes(act.view))
115
+ throw new Error(`user token: act.view "${String(act.view)}" is not a known view (${USER_TOKEN_VIEWS.join(", ")}) - unknown views fail closed`);
116
+ const scope = payload.scope === undefined ? [] : payload.scope;
117
+ if (!(Array.isArray(scope) && scope.every((s) => typeof s === "string")))
118
+ throw new Error("user token: scope must be a string list when present");
119
+ return {
120
+ owner,
121
+ space: opts.audience,
122
+ scope,
123
+ act: { owner: act.owner, actor: act.actor, scope: act.scope, parent: act.parent, view: act.view },
124
+ ver: USER_TOKEN_VER,
125
+ exp: payload.exp,
126
+ };
127
+ }
128
+ //# sourceMappingURL=token.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"token.js","sourceRoot":"","sources":["../src/token.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AACH,OAAO,EAAE,qBAAqB,EAAE,SAAS,EAAE,MAAM,MAAM,CAAC;AAExD,OAAO,EAAE,uBAAuB,EAAE,qBAAqB,EAAE,MAAM,gBAAgB,CAAC;AAEhF;4EAC4E;AAC5E,MAAM,CAAC,MAAM,cAAc,GAAG,CAAC,CAAC;AAEhC;8CAC8C;AAC9C,MAAM,CAAC,MAAM,iBAAiB,GAAG,GAAG,CAAC;AAErC;;;;;;+FAM+F;AAC/F,MAAM,CAAC,MAAM,gBAAgB,GAAG,CAAC,OAAO,EAAE,QAAQ,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,UAAU,CAAU,CAAC;AAG7G;;;;+FAI+F;AAC/F,MAAM,CAAC,MAAM,mBAAmB,GAA6C;IAC3E,KAAK,EAAE,OAAO;IACd,MAAM,EAAE,OAAO;IACf,gBAAgB,EAAE,OAAO;IACzB,gBAAgB,EAAE,OAAO;IACzB,QAAQ,EAAE,OAAO;CAClB,CAAC;AA8CF;;mBAEmB;AACnB,MAAM,CAAC,KAAK,UAAU,iBAAiB,CAAC,KAAa,EAAE,IAA2B;IAChF,MAAM,MAAM,GAAG,qBAAqB,CAAC,KAAK,CAAC,CAAC;IAC5C,IAAI,MAAM,CAAC,GAAG,KAAK,SAAS,IAAI,MAAM,CAAC,GAAG,KAAK,SAAS,IAAI,MAAM,CAAC,GAAG,KAAK,SAAS,IAAI,MAAM,CAAC,GAAG,KAAK,SAAS;QAC9G,MAAM,IAAI,KAAK,CAAC,yGAAyG,CAAC,CAAC;IAC7H,IAAI,MAAM,CAAC,GAAG,KAAK,OAAO;QAAE,MAAM,IAAI,KAAK,CAAC,sCAAsC,MAAM,CAAC,MAAM,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;IAEzG,MAAM,EAAE,OAAO,EAAE,GAAG,MAAM,SAAS,CAAC,KAAK,EAAE,IAAI,CAAC,GAAsB,EAAE;QACtE,UAAU,EAAE,CAAC,OAAO,CAAC;QACrB,MAAM,EAAE,IAAI,CAAC,MAAM;QACnB,QAAQ,EAAE,IAAI,CAAC,QAAQ;QACvB,cAAc,EAAE,IAAI,CAAC,iBAAiB,IAAI,CAAC;KAC5C,CAAC,CAAC;IAEH,8FAA8F;IAC9F,+FAA+F;IAC/F,2DAA2D;IAC3D,IAAI,OAAO,CAAC,GAAG,KAAK,IAAI,CAAC,QAAQ,IAAI,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,IAAI,OAAO,CAAC,GAAG,CAAC,MAAM,KAAK,CAAC,IAAI,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,KAAK,IAAI,CAAC,QAAQ,CAAC;QAChI,MAAM,IAAI,KAAK,CAAC,iFAAiF,CAAC,CAAC;IACrG,IAAI,OAAO,OAAO,CAAC,GAAG,KAAK,QAAQ;QAAE,MAAM,IAAI,KAAK,CAAC,0DAA0D,CAAC,CAAC;IACjH,IAAI,OAAO,OAAO,CAAC,GAAG,KAAK,QAAQ;QAAE,MAAM,IAAI,KAAK,CAAC,6BAA6B,CAAC,CAAC;IACpF,IAAI,OAAO,OAAO,CAAC,GAAG,KAAK,QAAQ;QAAE,MAAM,IAAI,KAAK,CAAC,6BAA6B,CAAC,CAAC;IACpF,+FAA+F;IAC/F,2FAA2F;IAC3F,+FAA+F;IAC/F,8FAA8F;IAC9F,6CAA6C;IAC7C,MAAM,MAAM,GAAG,IAAI,CAAC,SAAS,IAAI,iBAAiB,CAAC;IACnD,MAAM,GAAG,GAAG,IAAI,CAAC,iBAAiB,IAAI,CAAC,CAAC;IACxC,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC,CAAC;IAC1C,IAAI,OAAO,CAAC,GAAG,GAAG,GAAG,GAAG,GAAG;QAAE,MAAM,IAAI,KAAK,CAAC,kCAAkC,CAAC,CAAC;IACjF,IAAI,OAAO,CAAC,GAAG,GAAG,GAAG,GAAG,MAAM,GAAG,GAAG;QAClC,MAAM,IAAI,KAAK,CAAC,oCAAoC,MAAM,8DAA8D,CAAC,CAAC;IAC5H,IAAI,OAAO,CAAC,GAAG,GAAG,OAAO,CAAC,GAAG,GAAG,MAAM;QACpC,MAAM,IAAI,KAAK,CAAC,wBAAwB,OAAO,CAAC,GAAG,GAAG,OAAO,CAAC,GAAG,iBAAiB,MAAM,qDAAqD,CAAC,CAAC;IAEjJ,IAAI,OAAO,CAAC,GAAG,KAAK,cAAc;QAChC,MAAM,IAAI,KAAK,CAAC,mBAAmB,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,OAAO,cAAc,qDAAqD,CAAC,CAAC;IAEpI,IAAI,OAAO,OAAO,CAAC,GAAG,KAAK,QAAQ;QAAE,MAAM,IAAI,KAAK,CAAC,0EAA0E,CAAC,CAAC;IACjI,MAAM,KAAK,GAAG,uBAAuB,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;IAEnD,MAAM,GAAG,GAAG,OAAO,CAAC,GAAiC,CAAC;IACtD,IAAI,CAAC,GAAG,IAAI,OAAO,GAAG,KAAK,QAAQ;QAAE,MAAM,IAAI,KAAK,CAAC,iEAAiE,CAAC,CAAC;IACxH,IAAI,GAAG,CAAC,KAAK,KAAK,KAAK;QAAE,MAAM,IAAI,KAAK,CAAC,0BAA0B,MAAM,CAAC,GAAG,CAAC,KAAK,CAAC,aAAa,KAAK,4BAA4B,CAAC,CAAC;IACpI,IAAI,OAAO,GAAG,CAAC,KAAK,KAAK,QAAQ;QAAE,MAAM,IAAI,KAAK,CAAC,gFAAgF,CAAC,CAAC;IACrI,qBAAqB,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;IACjC,IAAI,GAAG,CAAC,KAAK,KAAK,SAAS,IAAI,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,GAAG,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,OAAO,CAAC,KAAK,QAAQ,CAAC,CAAC;QACzG,MAAM,IAAI,KAAK,CAAC,0DAA0D,CAAC,CAAC;IAC9E,IAAI,GAAG,CAAC,MAAM,KAAK,SAAS,EAAE,CAAC;QAC7B,6FAA6F;QAC7F,yFAAyF;QACzF,IAAI,OAAO,GAAG,CAAC,MAAM,KAAK,QAAQ;YAChC,MAAM,IAAI,KAAK,CAAC,8FAA8F,CAAC,CAAC;QAClH,MAAM,KAAK,GAAG,GAAG,CAAC,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;QACpC,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC;YACpB,MAAM,IAAI,KAAK,CAAC,2BAA2B,GAAG,CAAC,MAAM,iDAAiD,CAAC,CAAC;QAC1G,uBAAuB,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;QAClC,qBAAqB,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;IAClC,CAAC;IACD,IAAI,GAAG,CAAC,IAAI,KAAK,SAAS,IAAI,CAAC,gBAAgB,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC;QAChE,MAAM,IAAI,KAAK,CACb,yBAAyB,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,0BAA0B,gBAAgB,CAAC,IAAI,CAAC,IAAI,CAAC,+BAA+B,CAC9H,CAAC;IAEJ,MAAM,KAAK,GAAG,OAAO,CAAC,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,KAAK,CAAC;IAC/D,IAAI,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,IAAI,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,OAAO,CAAC,KAAK,QAAQ,CAAC,CAAC;QACtE,MAAM,IAAI,KAAK,CAAC,sDAAsD,CAAC,CAAC;IAE1E,OAAO;QACL,KAAK;QACL,KAAK,EAAE,IAAI,CAAC,QAAQ;QACpB,KAAK;QACL,GAAG,EAAE,EAAE,KAAK,EAAE,GAAG,CAAC,KAAK,EAAE,KAAK,EAAE,GAAG,CAAC,KAAK,EAAE,KAAK,EAAE,GAAG,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,CAAC,MAAM,EAAE,IAAI,EAAE,GAAG,CAAC,IAAI,EAAE;QACjG,GAAG,EAAE,cAAc;QACnB,GAAG,EAAE,OAAO,CAAC,GAAG;KACjB,CAAC;AACJ,CAAC"}
package/package.json CHANGED
@@ -1,11 +1,41 @@
1
1
  {
2
2
  "name": "@cotal-ai/auth",
3
- "version": "0.0.0",
4
- "description": "Placeholder for @cotal-ai/auth (Cotal per-user authentication). The first real release is published from the Cotal monorepo.",
3
+ "description": "Cotal per-user auth bridge: opaque owner-token derivation and strict user-token validation for the NATS auth-callout cutover (Plane 1 ↔ Plane 2).",
4
+ "version": "0.11.0",
5
5
  "license": "Apache-2.0",
6
- "homepage": "https://github.com/Cotal-AI/Cotal",
7
6
  "repository": {
8
7
  "type": "git",
9
- "url": "git+https://github.com/Cotal-AI/Cotal.git"
8
+ "url": "https://github.com/Cotal-AI/Cotal.git",
9
+ "directory": "implementations/auth"
10
+ },
11
+ "type": "module",
12
+ "main": "./dist/index.js",
13
+ "types": "./dist/index.d.ts",
14
+ "exports": {
15
+ ".": {
16
+ "types": "./dist/index.d.ts",
17
+ "import": "./dist/index.js"
18
+ }
19
+ },
20
+ "dependencies": {
21
+ "@nats-io/jwt": "0.0.10-5",
22
+ "@nats-io/nkeys": "^2.0.3",
23
+ "@nats-io/transport-node": "^3.4.0",
24
+ "jose": "^6.2.3",
25
+ "@cotal-ai/core": "0.11.0",
26
+ "@cotal-ai/workspace": "0.11.0"
27
+ },
28
+ "devDependencies": {
29
+ "better-auth": "^1.6.23"
30
+ },
31
+ "files": [
32
+ "dist"
33
+ ],
34
+ "publishConfig": {
35
+ "access": "public"
36
+ },
37
+ "scripts": {
38
+ "typecheck": "tsc -p tsconfig.json --noEmit",
39
+ "build": "tsc -p tsconfig.json"
10
40
  }
11
- }
41
+ }
package/README.md DELETED
@@ -1,4 +0,0 @@
1
- # @cotal-ai/auth
2
-
3
- Placeholder release to reserve the package name. The real `@cotal-ai/auth` (Cotal per-user
4
- authentication) is published from the [Cotal monorepo](https://github.com/Cotal-AI/Cotal).