@kybernesis/identity 0.1.1 → 0.2.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.
package/README.md CHANGED
@@ -1,31 +1,35 @@
1
1
  # @kybernesis/identity
2
2
 
3
- Give an eve agent its `.agent` name. ARP Cloud holds the identity, the key, the
4
- permissions and the audit trail; this package is the thin end: it verifies the
5
- tokens ARP Cloud signs when it delivers a message, serves the document ARP Cloud
6
- checks when you attach the runtime, and turns your paired peers into tools.
3
+ Give an eve agent its `.agent` name.
7
4
 
8
- No daemon. No socket. No private key in your deployment.
5
+ ARP Cloud holds the identity, the key, the permissions and the audit trail. This
6
+ package is the thin end that lives inside the agent: it lets the owner connect
7
+ the agent from the console with one click, accepts the messages ARP Cloud
8
+ delivers, and turns paired agents into tools.
9
9
 
10
- ## Install
10
+ No daemon. No socket. No private key in your deployment. No environment
11
+ variables to copy.
11
12
 
12
- ```bash
13
- eve add @kybernesis/identity-channel # agent/channels/arp.ts
14
- eve add @kybernesis/identity-peers # agent/tools/arp-peers.ts
15
- ```
13
+ ## For owners
16
14
 
17
- Then, in the ARP console (`cloud.arp.run/names/<name>` → Runtime → Attach), give
18
- it `https://<your-agent-host>/eve/v1/arp`. The console verifies the document this
19
- package serves and hands you four variables to set on the deployment:
15
+ 1. Register your name at [agent.arp.run](https://agent.arp.run).
16
+ 2. On the name's page, **Connect your agent**: paste the agent's address and
17
+ click Connect. That is all the agent receives its identity from ARP Cloud
18
+ and keeps it itself.
19
+ 3. **Pair** with another agent from the same page; paired agents show up as
20
+ `ask_<name>_agent` tools on the agent's next turn.
20
21
 
21
- ```
22
- ARP_ISSUER=https://gateway.arp.run
23
- ARP_AGENT_DID=did:web:<name>.agent
24
- ARP_AGENT_CREDENTIAL=<shown once>
25
- AGENTID_CHALLENGE=<from the attach step>
22
+ Agents built with Kybernesis tooling ship with this package already wired in.
23
+ For any other eve agent, the developer does the one-time setup below.
24
+
25
+ ## For developers (one-time)
26
+
27
+ ```bash
28
+ eve add @kybernesis/identity-channel # agent/channels/arp.ts → identityChannel()
29
+ eve add @kybernesis/identity-peers # agent/tools/arp-peers.ts → arpPeers()
26
30
  ```
27
31
 
28
- Add `arpAuth()` to your eve channel's auth walk so deliveries are accepted:
32
+ Then let deliveries in, on the eve channel:
29
33
 
30
34
  ```ts
31
35
  // agent/channels/eve.ts
@@ -34,23 +38,32 @@ import { localDev } from "eve/channels/auth";
34
38
  import { arpAuth } from "@kybernesis/identity";
35
39
 
36
40
  export default eveChannel({ auth: [arpAuth(), localDev()] });
41
+ // with @kybernesis/dispatch: dispatchChannel({ ..., extraAuth: [arpAuth(), ...] })
37
42
  ```
38
43
 
39
- (With `@kybernesis/dispatch`: `dispatchChannel({ trustedPeers, extraAuth: [arpAuth()] })`.)
44
+ Optionally append `ARP_INSTRUCTIONS` to the agent's instructions. Deploy once.
45
+ From here on the owner connects, pairs and revokes in the console; nothing in
46
+ the codebase changes.
47
+
48
+ ## How connect works
49
+
50
+ `POST /eve/v1/arp/connect` receives a short-lived ES256 token from ARP Cloud,
51
+ verifies it against `https://gateway.arp.run/.well-known/jwks.json`, checks it
52
+ was minted for this host, redeems it at the gateway for the agent's DID and
53
+ credential, and stores them in `.eve/arp-identity.json` (0600; path override
54
+ `ARP_IDENTITY_FILE`). `arpAuth()` and `arpPeers()` read that file per request,
55
+ so the connect takes effect without a restart.
56
+
57
+ An identity file written for another issuer is never overwritten; the owner
58
+ disconnects there first. Hosts without a writable disk (some serverless
59
+ platforms) keep the environment-variable path: `ARP_ISSUER`, `ARP_AGENT_DID`,
60
+ `ARP_AGENT_CREDENTIAL`, `AGENTID_CHALLENGE` (env always wins over the file).
40
61
 
41
62
  ## What each piece does
42
63
 
43
- - `arpAuth()` — route auth. A delivery from ARP Cloud carries an ES256 token
44
- verified against `https://gateway.arp.run/.well-known/jwks.json`, naming this
45
- agent as audience and the paired peer as subject. The session principal is
46
- `principalType: "agent"`, `authenticator: "arp"`, with `peerDid`,
47
- `connectionId`, `purpose` and `obligations` as attributes.
48
- - `identityChannel()` serves `GET /eve/v1/arp/.well-known/agentid-verification`
49
- (`{ did, challenge }`) and `/eve/v1/arp/health`.
50
- - `arpPeers()` — on every turn, lists active connections from ARP Cloud and
51
- exposes `ask_<peer>_agent` tools (the `_agent` suffix keeps them distinct from
52
- control-plane peers, which are `ask_<peer>`). Pair or revoke in the console; nothing to redeploy.
53
- - `ARP_INSTRUCTIONS` — append to your instructions so the model uses the tools well.
54
-
55
- Degrades, never throws at boot: with no credential there are simply no peer
56
- tools; with no token the auth entry skips to the next one.
64
+ - `identityChannel()` — `/eve/v1/arp/connect`, `/eve/v1/arp/.well-known/agentid-verification` (`{ did, challenge }`) and `/eve/v1/arp/health`.
65
+ - `arpAuth()` — route auth for deliveries: an ES256 token signed by ARP Cloud, verified offline, naming this agent as audience and the paired peer as subject. Session principal: `principalType: "agent"`, `authenticator: "arp"`, with `peerDid`, `connectionId`, `purpose`, `obligations`.
66
+ - `arpPeers()` on every turn, lists active connections and exposes `ask_<name>_agent` tools (the `_agent` suffix keeps them distinct from control-plane peers, which are `ask_<name>`).
67
+ - `ARP_INSTRUCTIONS` text to append to your instructions.
68
+
69
+ Degrades, never throws at boot: with no identity there are no peer tools and the auth entry skips to the next one.
package/dist/arp-auth.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { createLocalJWKSet, jwtVerify } from "jose";
2
2
  import { UnauthenticatedError, extractBearerToken } from "eve/channels/auth";
3
+ import { resolveIdentity } from "./store.js";
3
4
  /**
4
5
  * Route auth for messages ARP Cloud delivers to this agent.
5
6
  *
@@ -14,14 +15,15 @@ import { UnauthenticatedError, extractBearerToken } from "eve/channels/auth";
14
15
  * gateway outage is distinguishable from a bad credential.
15
16
  */
16
17
  export function arpAuth(options = {}) {
17
- const issuer = (options.issuer ?? process.env.ARP_ISSUER ?? "https://gateway.arp.run").replace(/\/+$/, "");
18
- const agentDid = options.agentDid ?? process.env.ARP_AGENT_DID ?? "";
18
+ // Identity is resolved per request (options env identity file) so a
19
+ // "Connect your agent" that lands mid-life takes effect without a restart.
20
+ const identity = () => resolveIdentity({ ...(options.issuer ? { issuer: options.issuer } : {}), ...(options.agentDid ? { agentDid: options.agentDid } : {}) });
19
21
  const fetchImpl = options.fetchImpl ?? globalThis.fetch;
20
22
  const ttl = options.jwksTtlMs ?? 5 * 60_000;
21
23
  let cached = null;
22
24
  // JWKS via global fetch (stubbable, same as the peers module) with a short
23
25
  // cache; one refetch when a key id is unknown (rotation).
24
- async function keySet(force = false) {
26
+ async function keySet(issuer, force = false) {
25
27
  if (!force && cached && Date.now() - cached.at < ttl)
26
28
  return cached.set;
27
29
  const res = await fetchImpl(`${issuer}/.well-known/jwks.json`, { signal: AbortSignal.timeout(6_000) });
@@ -33,6 +35,7 @@ export function arpAuth(options = {}) {
33
35
  }
34
36
  return async (request) => {
35
37
  const token = extractBearerToken(request.headers.get("authorization"));
38
+ const { did: agentDid, issuer } = identity();
36
39
  if (!token || !agentDid)
37
40
  return null;
38
41
  // Only ARP tokens are ours; anything else falls through to the next entry.
@@ -48,12 +51,12 @@ export function arpAuth(options = {}) {
48
51
  let payload;
49
52
  try {
50
53
  try {
51
- ({ payload } = await jwtVerify(token, await keySet(), { issuer, audience: agentDid }));
54
+ ({ payload } = await jwtVerify(token, await keySet(issuer), { issuer, audience: agentDid }));
52
55
  }
53
56
  catch (first) {
54
57
  const code = first?.code;
55
58
  if (code === "ERR_JWKS_NO_MATCHING_KEY") {
56
- ({ payload } = await jwtVerify(token, await keySet(true), { issuer, audience: agentDid }));
59
+ ({ payload } = await jwtVerify(token, await keySet(issuer, true), { issuer, audience: agentDid }));
57
60
  }
58
61
  else {
59
62
  throw first;
package/dist/channel.d.ts CHANGED
@@ -1,14 +1,19 @@
1
- export interface IdentityChannelOptions {
2
- /** This agent's identity, e.g. `did:web:samantha.agent`. Defaults to ARP_AGENT_DID. */
1
+ import { type ConnectOptions } from "./connect.js";
2
+ export interface IdentityChannelOptions extends ConnectOptions {
3
+ /** This agent's identity, e.g. `did:web:samantha.agent`. Defaults to ARP_AGENT_DID, then the identity file. */
3
4
  agentDid?: string;
4
- /** The challenge ARP Cloud issued when attaching this runtime. Defaults to AGENTID_CHALLENGE. */
5
+ /** The challenge ARP Cloud issued when attaching this runtime. Defaults to AGENTID_CHALLENGE, then the identity file. */
5
6
  challenge?: string;
6
7
  }
7
8
  export declare const IDENTITY_PREFIX = "/eve/v1/arp";
8
9
  /**
9
- * Serves the identity verification document ARP Cloud fetches when this
10
- * runtime is attached to a name (`<url>/.well-known/agentid-verification`),
11
- * plus a health probe. Mount as `agent/channels/arp.ts`; attach the name with
12
- * URL `https://<host>/eve/v1/arp`.
10
+ * This agent's .agent identity surface:
11
+ *
12
+ * POST /eve/v1/arp/connect — "Connect your agent" (ARP Cloud calls it)
13
+ * GET /eve/v1/arp/.well-known/agentid-verification — { did, challenge } ARP Cloud checks
14
+ * GET /eve/v1/arp/health — { ok, did, store }
15
+ *
16
+ * Mount as `agent/channels/arp.ts`. Deliveries from paired agents arrive on
17
+ * the eve channel (add `arpAuth()` there), not here.
13
18
  */
14
19
  export declare function identityChannel(options?: IdentityChannelOptions): import("eve/channels").Channel<undefined, Record<string, unknown>, Record<string, unknown>>;
package/dist/channel.js CHANGED
@@ -1,26 +1,41 @@
1
- import { defineChannel, GET } from "eve/channels";
1
+ import { defineChannel, GET, POST } from "eve/channels";
2
+ import { handleConnect, requestHostOf } from "./connect.js";
3
+ import { resolveIdentity, storeKind } from "./store.js";
2
4
  export const IDENTITY_PREFIX = "/eve/v1/arp";
3
5
  /**
4
- * Serves the identity verification document ARP Cloud fetches when this
5
- * runtime is attached to a name (`<url>/.well-known/agentid-verification`),
6
- * plus a health probe. Mount as `agent/channels/arp.ts`; attach the name with
7
- * URL `https://<host>/eve/v1/arp`.
6
+ * This agent's .agent identity surface:
7
+ *
8
+ * POST /eve/v1/arp/connect — "Connect your agent" (ARP Cloud calls it)
9
+ * GET /eve/v1/arp/.well-known/agentid-verification — { did, challenge } ARP Cloud checks
10
+ * GET /eve/v1/arp/health — { ok, did, store }
11
+ *
12
+ * Mount as `agent/channels/arp.ts`. Deliveries from paired agents arrive on
13
+ * the eve channel (add `arpAuth()` there), not here.
8
14
  */
9
15
  export function identityChannel(options = {}) {
10
16
  return defineChannel({
11
- // Deliveries from ARP Cloud arrive on the eve channel (POST /eve/v1/session),
12
- // not here; this channel only owns the identity's HTTP documents.
13
17
  receive: async ({ message, auth }, { from }) => from("arp:inbox").send(message, { auth }),
14
18
  routes: [
19
+ POST(IDENTITY_PREFIX + "/connect", async (req) => {
20
+ let body = {};
21
+ try {
22
+ body = (await req.json());
23
+ }
24
+ catch { /* bad json → 400 below */ }
25
+ const { status, body: out } = await handleConnect(body, requestHostOf(req), options);
26
+ return Response.json(out, { status, headers: { "cache-control": "no-store" } });
27
+ }),
15
28
  GET(IDENTITY_PREFIX + "/.well-known/agentid-verification", async () => {
16
- const did = options.agentDid ?? process.env.ARP_AGENT_DID ?? "";
17
- const challenge = options.challenge ?? process.env.AGENTID_CHALLENGE ?? "";
18
- if (!did || !challenge) {
19
- return Response.json({ error: "not_configured", hint: "set ARP_AGENT_DID and AGENTID_CHALLENGE" }, { status: 404 });
29
+ const id = resolveIdentity(options);
30
+ if (!id.did || !id.challenge) {
31
+ return Response.json({ error: "not_configured", hint: "connect this agent to its name in the ARP console" }, { status: 404 });
20
32
  }
21
- return Response.json({ did, challenge }, { headers: { "cache-control": "no-store" } });
33
+ return Response.json({ did: id.did, challenge: id.challenge }, { headers: { "cache-control": "no-store" } });
34
+ }),
35
+ GET(IDENTITY_PREFIX + "/health", async () => {
36
+ const id = resolveIdentity(options);
37
+ return Response.json({ ok: true, did: id.did || null, source: id.source, store: storeKind() });
22
38
  }),
23
- GET(IDENTITY_PREFIX + "/health", async () => Response.json({ ok: true, did: options.agentDid ?? process.env.ARP_AGENT_DID ?? null })),
24
39
  ],
25
40
  });
26
41
  }
@@ -0,0 +1,25 @@
1
+ export interface ConnectOptions {
2
+ /** Issuers this runtime accepts connect tokens from. Default: ARP_ISSUER or https://gateway.arp.run. */
3
+ issuers?: string[];
4
+ fetchImpl?: typeof fetch;
5
+ }
6
+ export interface ConnectRequest {
7
+ token?: string;
8
+ issuer?: string;
9
+ }
10
+ /**
11
+ * "Connect your agent" — the runtime's half.
12
+ *
13
+ * ARP Cloud posts a short-lived ES256 connect token here. We verify it
14
+ * against the issuer's JWKS (offline trust, no shared secret), check it was
15
+ * minted for THIS host, redeem it at `<issuer>/agent-api/bootstrap` for the
16
+ * agent's DID + credential, and keep those in the identity file. From then
17
+ * on `arpAuth()` accepts deliveries and `arpPeers()` can call peers — with
18
+ * no environment variable and no restart.
19
+ */
20
+ export declare function handleConnect(body: ConnectRequest, requestHost: string | null, options?: ConnectOptions): Promise<{
21
+ status: number;
22
+ body: Record<string, unknown>;
23
+ }>;
24
+ /** Host of the incoming request (proxy-aware), for the audience check. */
25
+ export declare function requestHostOf(req: Request): string | null;
@@ -0,0 +1,91 @@
1
+ import { createLocalJWKSet, jwtVerify } from "jose";
2
+ import { DEFAULT_ISSUER, readIdentityFile, saveIdentity, storeKind } from "./store.js";
3
+ /**
4
+ * "Connect your agent" — the runtime's half.
5
+ *
6
+ * ARP Cloud posts a short-lived ES256 connect token here. We verify it
7
+ * against the issuer's JWKS (offline trust, no shared secret), check it was
8
+ * minted for THIS host, redeem it at `<issuer>/agent-api/bootstrap` for the
9
+ * agent's DID + credential, and keep those in the identity file. From then
10
+ * on `arpAuth()` accepts deliveries and `arpPeers()` can call peers — with
11
+ * no environment variable and no restart.
12
+ */
13
+ export async function handleConnect(body, requestHost, options = {}) {
14
+ const fetchImpl = options.fetchImpl ?? globalThis.fetch;
15
+ const allowed = (options.issuers ?? [process.env.ARP_ISSUER ?? DEFAULT_ISSUER]).map((i) => i.replace(/\/+$/, ""));
16
+ const issuer = (body.issuer ?? "").replace(/\/+$/, "");
17
+ const token = body.token ?? "";
18
+ if (!token || !issuer)
19
+ return { status: 400, body: { ok: false, error: "bad_request" } };
20
+ if (!allowed.includes(issuer))
21
+ return { status: 403, body: { ok: false, error: "unknown_issuer" } };
22
+ // First bind wins: an identity file written by another issuer is not
23
+ // overwritten (the owner disconnects there first).
24
+ const bound = readIdentityFile();
25
+ if (bound.did && bound.issuer && bound.issuer.replace(/\/+$/, "") !== issuer) {
26
+ return { status: 409, body: { ok: false, error: "bound_to_another_issuer" } };
27
+ }
28
+ let payload;
29
+ try {
30
+ const res = await fetchImpl(`${issuer}/.well-known/jwks.json`, { signal: AbortSignal.timeout(6_000) });
31
+ if (!res.ok)
32
+ throw new Error(`jwks ${res.status}`);
33
+ const jwks = createLocalJWKSet((await res.json()));
34
+ ({ payload } = await jwtVerify(token, jwks, { issuer }));
35
+ }
36
+ catch (err) {
37
+ return { status: 401, body: { ok: false, error: "invalid_token", detail: err.message } };
38
+ }
39
+ if (payload.kind !== "arp-connect" || typeof payload.did !== "string")
40
+ return { status: 401, body: { ok: false, error: "invalid_token" } };
41
+ // The token names the host it was minted for; refuse one minted for a different origin.
42
+ const aud = Array.isArray(payload.aud) ? payload.aud[0] : payload.aud;
43
+ if (requestHost && aud) {
44
+ let audHost = "";
45
+ try {
46
+ audHost = new URL(aud).host;
47
+ }
48
+ catch {
49
+ audHost = aud;
50
+ }
51
+ if (audHost.toLowerCase() !== requestHost.toLowerCase())
52
+ return { status: 401, body: { ok: false, error: "audience_mismatch", detail: `token is for ${audHost}` } };
53
+ }
54
+ if (storeKind() === "none" && !process.env.ARP_AGENT_CREDENTIAL) {
55
+ return { status: 503, body: { ok: false, error: "store_unwritable", store: "none" } };
56
+ }
57
+ let cfg;
58
+ let redeem;
59
+ try {
60
+ redeem = await fetchImpl(`${issuer}/agent-api/bootstrap`, { method: "POST", headers: { authorization: `Bearer ${token}` }, signal: AbortSignal.timeout(10_000) });
61
+ cfg = (await redeem.json().catch(() => ({})));
62
+ }
63
+ catch (err) {
64
+ return { status: 502, body: { ok: false, error: "issuer_unreachable", detail: err.message } };
65
+ }
66
+ if (!redeem.ok || !cfg.did || !cfg.credential || !cfg.challenge) {
67
+ return { status: 502, body: { ok: false, error: cfg.error ?? "bootstrap_failed" } };
68
+ }
69
+ try {
70
+ saveIdentity({ did: cfg.did, issuer: cfg.issuer ?? issuer, credential: cfg.credential, challenge: cfg.challenge });
71
+ }
72
+ catch (err) {
73
+ return { status: 503, body: { ok: false, error: "store_unwritable", store: "none", detail: err.message } };
74
+ }
75
+ return { status: 200, body: { ok: true, did: cfg.did, store: "file" } };
76
+ }
77
+ /** Host of the incoming request (proxy-aware), for the audience check. */
78
+ export function requestHostOf(req) {
79
+ const fwd = req.headers.get("x-forwarded-host");
80
+ if (fwd)
81
+ return fwd.split(",")[0].trim();
82
+ const host = req.headers.get("host");
83
+ if (host)
84
+ return host;
85
+ try {
86
+ return new URL(req.url).host;
87
+ }
88
+ catch {
89
+ return null;
90
+ }
91
+ }
package/dist/index.d.ts CHANGED
@@ -2,3 +2,5 @@ export { arpAuth, type ArpAuthOptions } from "./arp-auth.js";
2
2
  export { identityChannel, IDENTITY_PREFIX, type IdentityChannelOptions } from "./channel.js";
3
3
  export { arpPeers, discoverPeers, askPeer, toolName, type ArpPeersOptions, type ArpPeer } from "./peers.js";
4
4
  export { ARP_INSTRUCTIONS } from "./instructions.js";
5
+ export { resolveIdentity, readIdentityFile, saveIdentity, storeKind, identityFilePath, DEFAULT_ISSUER, type Identity } from "./store.js";
6
+ export { handleConnect, requestHostOf, type ConnectOptions, type ConnectRequest } from "./connect.js";
package/dist/index.js CHANGED
@@ -2,3 +2,5 @@ export { arpAuth } from "./arp-auth.js";
2
2
  export { identityChannel, IDENTITY_PREFIX } from "./channel.js";
3
3
  export { arpPeers, discoverPeers, askPeer, toolName } from "./peers.js";
4
4
  export { ARP_INSTRUCTIONS } from "./instructions.js";
5
+ export { resolveIdentity, readIdentityFile, saveIdentity, storeKind, identityFilePath, DEFAULT_ISSUER } from "./store.js";
6
+ export { handleConnect, requestHostOf } from "./connect.js";
@@ -5,7 +5,7 @@ export const ARP_INSTRUCTIONS = `
5
5
  ## Your .agent identity
6
6
 
7
7
  You have a registered name on the agent network. Other agents that are paired
8
- with you appear as \`ask_<name>_agent\` tools (the name is their .agent name); use them when a request belongs to that
8
+ with you appear as \`ask_<name>_agent\` tools (the name is their .agent name; a plain \`ask_<name>\` tool, if present, is a different, non-network peer); use them when a request belongs to that
9
9
  agent rather than to you. When a message arrives from a paired agent, the
10
10
  caller's identity has already been verified and the message has already been
11
11
  checked against what its owner allowed — answer it as you would a trusted
package/dist/peers.js CHANGED
@@ -1,10 +1,13 @@
1
1
  import { defineDynamic, defineTool } from "eve/tools";
2
2
  import { z } from "zod";
3
+ import { resolveIdentity } from "./store.js";
3
4
  const cache = new Map();
4
5
  function config(options) {
6
+ // Per call, so a "Connect your agent" takes effect on the next turn.
7
+ const id = resolveIdentity({ ...(options.issuer ? { issuer: options.issuer } : {}), ...(options.credential ? { credential: options.credential } : {}) });
5
8
  return {
6
- issuer: (options.issuer ?? process.env.ARP_ISSUER ?? "https://gateway.arp.run").replace(/\/+$/, ""),
7
- credential: options.credential ?? process.env.ARP_AGENT_CREDENTIAL ?? "",
9
+ issuer: id.issuer,
10
+ credential: id.credential,
8
11
  cacheMs: options.cacheMs ?? 60_000,
9
12
  timeoutMs: options.timeoutMs ?? 120_000,
10
13
  fetchImpl: options.fetchImpl ?? globalThis.fetch,
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Where this runtime keeps its .agent identity.
3
+ *
4
+ * Resolution order, per read: explicit options → environment (`ARP_AGENT_DID`,
5
+ * `ARP_ISSUER`, `ARP_AGENT_CREDENTIAL`, `AGENTID_CHALLENGE`) → the identity
6
+ * file. The file is what "Connect your agent" writes, so an owner never
7
+ * touches environment variables; env stays for hosts without a writable disk.
8
+ */
9
+ export interface Identity {
10
+ did: string;
11
+ issuer: string;
12
+ credential: string;
13
+ challenge: string;
14
+ /** Where the identity came from. */
15
+ source: "options" | "env" | "file" | "none";
16
+ }
17
+ export declare const DEFAULT_ISSUER = "https://gateway.arp.run";
18
+ export declare function identityFilePath(): string;
19
+ /** The identity file as written by "Connect your agent" ({} when absent). */
20
+ export declare function readIdentityFile(): Partial<Identity>;
21
+ /** Current identity. Never throws; missing pieces are empty strings. */
22
+ export declare function resolveIdentity(options?: {
23
+ agentDid?: string;
24
+ issuer?: string;
25
+ credential?: string;
26
+ challenge?: string;
27
+ }): Identity;
28
+ /** Persist a connected identity. Throws when the store is not writable. */
29
+ export declare function saveIdentity(identity: {
30
+ did: string;
31
+ issuer: string;
32
+ credential: string;
33
+ challenge: string;
34
+ }): string;
35
+ /** Can this host keep an identity on disk? */
36
+ export declare function storeKind(): "file" | "none";
package/dist/store.js ADDED
@@ -0,0 +1,68 @@
1
+ import { existsSync, mkdirSync, readFileSync, statSync, unlinkSync, writeFileSync } from "node:fs";
2
+ import { dirname, join } from "node:path";
3
+ export const DEFAULT_ISSUER = "https://gateway.arp.run";
4
+ export function identityFilePath() {
5
+ return process.env.ARP_IDENTITY_FILE ?? join(process.cwd(), ".eve", "arp-identity.json");
6
+ }
7
+ let cache = null;
8
+ /** The identity file as written by "Connect your agent" ({} when absent). */
9
+ export function readIdentityFile() {
10
+ const path = identityFilePath();
11
+ try {
12
+ const mtimeMs = statSync(path).mtimeMs;
13
+ if (cache && cache.path === path && cache.mtimeMs === mtimeMs)
14
+ return cache.value;
15
+ const value = JSON.parse(readFileSync(path, "utf8"));
16
+ cache = { path, mtimeMs, value };
17
+ return value;
18
+ }
19
+ catch {
20
+ cache = null;
21
+ return {};
22
+ }
23
+ }
24
+ /** Current identity. Never throws; missing pieces are empty strings. */
25
+ export function resolveIdentity(options = {}) {
26
+ const env = {
27
+ did: process.env.ARP_AGENT_DID ?? "",
28
+ issuer: process.env.ARP_ISSUER ?? "",
29
+ credential: process.env.ARP_AGENT_CREDENTIAL ?? "",
30
+ challenge: process.env.AGENTID_CHALLENGE ?? "",
31
+ };
32
+ const file = readIdentityFile();
33
+ const pick = (opt, e, f) => opt ? [opt, "options"] : e ? [e, "env"] : f ? [f, "file"] : ["", "none"];
34
+ const [did, didSource] = pick(options.agentDid, env.did, file.did);
35
+ const [issuer] = pick(options.issuer, env.issuer, file.issuer);
36
+ const [credential] = pick(options.credential, env.credential, file.credential);
37
+ const [challenge] = pick(options.challenge, env.challenge, file.challenge);
38
+ return { did, issuer: (issuer || DEFAULT_ISSUER).replace(/\/+$/, ""), credential, challenge, source: didSource };
39
+ }
40
+ /** Persist a connected identity. Throws when the store is not writable. */
41
+ export function saveIdentity(identity) {
42
+ const path = identityFilePath();
43
+ const dir = dirname(path);
44
+ if (!existsSync(dir))
45
+ mkdirSync(dir, { recursive: true, mode: 0o700 });
46
+ writeFileSync(path, JSON.stringify({ ...identity, connected_at: new Date().toISOString() }, null, 2) + "\n", { mode: 0o600 });
47
+ cache = null;
48
+ return path;
49
+ }
50
+ /** Can this host keep an identity on disk? */
51
+ export function storeKind() {
52
+ const path = identityFilePath();
53
+ try {
54
+ const dir = dirname(path);
55
+ if (!existsSync(dir))
56
+ mkdirSync(dir, { recursive: true, mode: 0o700 });
57
+ const probe = join(dir, `.arp-probe-${process.pid}`);
58
+ writeFileSync(probe, "", { mode: 0o600 });
59
+ try {
60
+ unlinkSync(probe);
61
+ }
62
+ catch { /* ignore */ }
63
+ return "file";
64
+ }
65
+ catch {
66
+ return "none";
67
+ }
68
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kybernesis/identity",
3
- "version": "0.1.1",
3
+ "version": "0.2.0",
4
4
  "description": "Give an eve agent its .agent identity: verify ARP Cloud push tokens, serve the verification document, and reach paired peers as tools \u2014 no daemon, no socket, no key in the bundle.",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",