@saadnvd1/agent-os 0.3.561 → 0.3.565

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.
@@ -1,9 +1,6 @@
1
1
  import { NextRequest, NextResponse } from "next/server";
2
- import {
3
- renameDevice,
4
- revokeDevice,
5
- setDeviceCanApprove,
6
- } from "@/lib/security/devices";
2
+ import { renameDevice, setDeviceCanApprove } from "@/lib/security/devices";
3
+ import { revokeDeviceAndKeys } from "@/lib/orchestrator/passkey-asks";
7
4
  import { requireLocalTrust } from "@/lib/security/route-guard";
8
5
  import { DEVICE_HEADER } from "@/lib/security/auth";
9
6
  import { demoMode } from "@/lib/security/demo";
@@ -43,7 +40,7 @@ export async function DELETE(request: NextRequest, { params }: Ctx) {
43
40
  { error: "Not available in the demo." },
44
41
  { status: 403 }
45
42
  );
46
- if (!revokeDevice(id))
43
+ if (!revokeDeviceAndKeys(id, "Devices"))
47
44
  return NextResponse.json({ error: "Not found" }, { status: 404 });
48
45
  return NextResponse.json({ ok: true });
49
46
  }
@@ -1,12 +1,12 @@
1
1
  import { NextRequest, NextResponse } from "next/server";
2
- import { revokeDevice } from "@/lib/security/devices";
2
+ import { revokeDeviceAndKeys } from "@/lib/orchestrator/passkey-asks";
3
3
  import { callingDevice, needsDevice } from "@/lib/security/device-me";
4
4
 
5
5
  // DELETE /api/devices/me - the device signs itself out ("Forget machine")
6
6
  export async function DELETE(request: NextRequest) {
7
7
  const id = callingDevice(request);
8
8
  if (!id) return needsDevice();
9
- if (!revokeDevice(id))
9
+ if (!revokeDeviceAndKeys(id, "the device itself"))
10
10
  return NextResponse.json({ error: "Not found" }, { status: 404 });
11
11
  return NextResponse.json({ ok: true });
12
12
  }
@@ -0,0 +1,36 @@
1
+ import { NextRequest, NextResponse } from "next/server";
2
+ import { requireApprover } from "@/lib/security/approver";
3
+ import {
4
+ deviceRegistrationOptions,
5
+ PresenceError,
6
+ } from "@/lib/security/presence";
7
+ import { refusalResponse } from "@/lib/security/presence-http";
8
+ import { passkeysBootstrapped } from "@/lib/security/passkeys";
9
+
10
+ // A challenge for this paired device's new approval key to sign. The key is
11
+ // the device's own, so only a paired device asks; with passkeys already set
12
+ // up, it needs a code from one of them, like a browser passkey does.
13
+ export async function POST(request: NextRequest) {
14
+ const gate = requireApprover(request);
15
+ if (!gate.ok) return gate.response;
16
+ if (gate.approver.via !== "device" || !gate.approver.deviceId)
17
+ return NextResponse.json(
18
+ { error: "Only a paired device has its own key" },
19
+ { status: 403 }
20
+ );
21
+ try {
22
+ const { enrollCode } = (await request.json().catch(() => ({}))) as {
23
+ enrollCode?: string;
24
+ };
25
+ return NextResponse.json(
26
+ deviceRegistrationOptions(gate.approver.deviceId, enrollCode)
27
+ );
28
+ } catch (error) {
29
+ if (error instanceof PresenceError && error.code === "enroll")
30
+ return NextResponse.json({
31
+ needsCode: true,
32
+ canBootstrap: !passkeysBootstrapped(),
33
+ });
34
+ return refusalResponse(error);
35
+ }
36
+ }
@@ -0,0 +1,55 @@
1
+ import { NextRequest, NextResponse } from "next/server";
2
+ import { requireApprover } from "@/lib/security/approver";
3
+ import { verifyDeviceRegistration } from "@/lib/security/presence";
4
+ import { placeOf, refusalResponse } from "@/lib/security/presence-http";
5
+ import { raisePasskeyAsks } from "@/lib/orchestrator/passkey-asks";
6
+ import { resolvePasskeyAsks } from "@/lib/orchestrator/asks";
7
+
8
+ // Stores the paired device's own approval key, and puts it on Saad's asks
9
+ // list to confirm. A key it replaces (a reinstall) is revoked and noted.
10
+ export async function POST(request: NextRequest) {
11
+ const gate = requireApprover(request);
12
+ if (!gate.ok) return gate.response;
13
+ if (gate.approver.via !== "device" || !gate.approver.deviceId)
14
+ return NextResponse.json(
15
+ { error: "Only a paired device has its own key" },
16
+ { status: 403 }
17
+ );
18
+ try {
19
+ const body = (await request.json().catch(() => ({}))) as {
20
+ publicKey?: string;
21
+ challenge?: string;
22
+ signature?: string;
23
+ name?: string;
24
+ hardware?: string;
25
+ enrollCode?: string;
26
+ };
27
+ if (!body.publicKey || !body.challenge || !body.signature)
28
+ return NextResponse.json({ error: "No key" }, { status: 400 });
29
+ const place = placeOf(gate.approver, request.headers);
30
+ const { key, replaced } = verifyDeviceRegistration(
31
+ gate.approver.deviceId,
32
+ {
33
+ publicKey: body.publicKey,
34
+ challenge: body.challenge,
35
+ signature: body.signature,
36
+ name: body.name || place,
37
+ hardware: body.hardware,
38
+ },
39
+ {
40
+ via: gate.approver.via,
41
+ from: place,
42
+ userAgent: request.headers.get("user-agent"),
43
+ },
44
+ body.enrollCode
45
+ );
46
+ for (const old of replaced) {
47
+ resolvePasskeyAsks(old.id, "replaced");
48
+ raisePasskeyAsks(old, place, "revoked");
49
+ }
50
+ raisePasskeyAsks(key, place);
51
+ return NextResponse.json({ passkey: { id: key.id, name: key.name } });
52
+ } catch (error) {
53
+ return refusalResponse(error);
54
+ }
55
+ }
@@ -1,12 +1,11 @@
1
1
  import { NextRequest, NextResponse } from "next/server";
2
- import type { AuthenticationResponseJSON } from "@simplewebauthn/server";
3
2
  import { requireApprover } from "@/lib/security/approver";
3
+ import { mintEnrollCode } from "@/lib/security/presence";
4
4
  import {
5
- mintEnrollCode,
6
- relyingParty,
7
- verifyPresence,
8
- } from "@/lib/security/presence";
9
- import { refusalResponse } from "@/lib/security/presence-http";
5
+ refusalResponse,
6
+ requirePresence,
7
+ type PresenceProof,
8
+ } from "@/lib/security/presence-http";
10
9
 
11
10
  // A one-time code (10 minutes) for adding a passkey on another device,
12
11
  // minted only on an assertion from an existing passkey.
@@ -14,15 +13,8 @@ export async function POST(request: NextRequest) {
14
13
  const gate = requireApprover(request);
15
14
  if (!gate.ok) return gate.response;
16
15
  try {
17
- const { assertion } = (await request.json().catch(() => ({}))) as {
18
- assertion?: AuthenticationResponseJSON;
19
- };
20
- await verifyPresence(
21
- relyingParty(request.headers),
22
- assertion,
23
- "enroll",
24
- "enroll"
25
- );
16
+ const body = (await request.json().catch(() => ({}))) as PresenceProof;
17
+ await requirePresence(request, gate.approver, body, "enroll", "enroll");
26
18
  return NextResponse.json({ code: mintEnrollCode() });
27
19
  } catch (error) {
28
20
  return refusalResponse(error);
@@ -2,6 +2,7 @@ import { NextRequest, NextResponse } from "next/server";
2
2
  import { requireApprover } from "@/lib/security/approver";
3
3
  import {
4
4
  assertionOptions,
5
+ deviceAssertionOptions,
5
6
  relyingParty,
6
7
  type PresencePurpose,
7
8
  } from "@/lib/security/presence";
@@ -21,8 +22,27 @@ export async function POST(request: NextRequest) {
21
22
  askId?: number;
22
23
  binding?: string;
23
24
  passkeyId?: string;
25
+ method?: "device";
24
26
  };
25
27
  const binding = presenceBinding(body);
28
+ if (body.method === "device") {
29
+ if (gate.approver.via !== "device" || !gate.approver.deviceId)
30
+ return NextResponse.json(
31
+ { error: "Only a paired device has its own key" },
32
+ { status: 403 }
33
+ );
34
+ const device = deviceAssertionOptions(
35
+ gate.approver.deviceId,
36
+ body.purpose!,
37
+ binding
38
+ );
39
+ if (!device)
40
+ return NextResponse.json({
41
+ needsDeviceKey: true,
42
+ canBootstrap: !passkeysBootstrapped(),
43
+ });
44
+ return NextResponse.json({ device });
45
+ }
26
46
  const rp = relyingParty(request.headers);
27
47
  const options = await assertionOptions(rp, body.purpose!, binding);
28
48
  if (!options)
@@ -1,9 +1,12 @@
1
1
  import { NextRequest, NextResponse } from "next/server";
2
- import type { AuthenticationResponseJSON } from "@simplewebauthn/server";
3
2
  import { requireApprover } from "@/lib/security/approver";
4
3
  import { getPasskey, revokePasskey } from "@/lib/security/passkeys";
5
- import { relyingParty, verifyPresence } from "@/lib/security/presence";
6
- import { placeOf, refusalResponse } from "@/lib/security/presence-http";
4
+ import {
5
+ placeOf,
6
+ refusalResponse,
7
+ requirePresence,
8
+ type PresenceProof,
9
+ } from "@/lib/security/presence-http";
7
10
  import { raisePasskeyAsks } from "@/lib/orchestrator/passkey-asks";
8
11
  import { revokePresence } from "@/lib/orchestrator/presence-binding";
9
12
  import { resolvePasskeyAsks } from "@/lib/orchestrator/asks";
@@ -21,12 +24,11 @@ export async function DELETE(request: NextRequest, { params }: Ctx) {
21
24
  const key = getPasskey(id);
22
25
  if (!key || key.revoked_at)
23
26
  return NextResponse.json({ error: "Not found" }, { status: 404 });
24
- const { assertion } = (await request.json().catch(() => ({}))) as {
25
- assertion?: AuthenticationResponseJSON;
26
- };
27
- await verifyPresence(
28
- relyingParty(request.headers),
29
- assertion,
27
+ const body = (await request.json().catch(() => ({}))) as PresenceProof;
28
+ await requirePresence(
29
+ request,
30
+ gate.approver,
31
+ body,
30
32
  "revoke",
31
33
  revokePresence(id)
32
34
  );
@@ -1,8 +1,11 @@
1
1
  import { NextRequest, NextResponse } from "next/server";
2
- import type { AuthenticationResponseJSON } from "@simplewebauthn/server";
3
2
  import { requireApprover } from "@/lib/security/approver";
4
- import { relyingParty, verifyPresence } from "@/lib/security/presence";
5
- import { placeOf, refusalResponse } from "@/lib/security/presence-http";
3
+ import {
4
+ placeOf,
5
+ refusalResponse,
6
+ requirePresence,
7
+ type PresenceProof,
8
+ } from "@/lib/security/presence-http";
6
9
  import { getPasskey } from "@/lib/security/passkeys";
7
10
  import { raisePasskeyAsks } from "@/lib/orchestrator/passkey-asks";
8
11
  import {
@@ -40,10 +43,11 @@ export async function POST(request: NextRequest, { params }: RouteParams) {
40
43
  : requireApprover(request);
41
44
  if (!gate.ok) return gate.response;
42
45
  const { id, askId } = await params;
43
- const body = (await request.json().catch(() => null)) as {
44
- binding?: string;
45
- assertion?: AuthenticationResponseJSON;
46
- } | null;
46
+ const body = (await request.json().catch(() => null)) as
47
+ | ({
48
+ binding?: string;
49
+ } & PresenceProof)
50
+ | null;
47
51
  const parsed = parseAnswer(body);
48
52
  // A demo keeps no visitor's words where the next visitor would read them.
49
53
  const answer =
@@ -73,9 +77,10 @@ export async function POST(request: NextRequest, { params }: RouteParams) {
73
77
  { status: 400 }
74
78
  );
75
79
  if (!demo && PRESENCE_KINDS.includes(ask.kind))
76
- await verifyPresence(
77
- relyingParty(request.headers),
78
- body.assertion,
80
+ await requirePresence(
81
+ request,
82
+ gate.approver,
83
+ body,
79
84
  "approve",
80
85
  askPresence(ask.id, body.binding)
81
86
  );
@@ -1,10 +1,12 @@
1
1
  import { NextRequest, NextResponse } from "next/server";
2
- import type { AuthenticationResponseJSON } from "@simplewebauthn/server";
3
2
  import { getWorkspace } from "@/lib/workspaces";
4
3
  import { setPaused } from "@/lib/orchestrator/pause";
5
4
  import { requireApprover } from "@/lib/security/approver";
6
- import { relyingParty, verifyPresence } from "@/lib/security/presence";
7
- import { refusalResponse } from "@/lib/security/presence-http";
5
+ import {
6
+ refusalResponse,
7
+ requirePresence,
8
+ type PresenceProof,
9
+ } from "@/lib/security/presence-http";
8
10
  import { resumePresence } from "@/lib/orchestrator/presence-binding";
9
11
 
10
12
  type RouteParams = { params: Promise<{ id: string }> };
@@ -18,10 +20,10 @@ export async function POST(request: NextRequest, { params }: RouteParams) {
18
20
  const { id } = await params;
19
21
  if (!getWorkspace(id))
20
22
  return NextResponse.json({ error: "Workspace not found" }, { status: 404 });
21
- const { paused, assertion } = (await request.json().catch(() => ({}))) as {
23
+ const body = (await request.json().catch(() => ({}))) as {
22
24
  paused?: unknown;
23
- assertion?: AuthenticationResponseJSON;
24
- };
25
+ } & PresenceProof;
26
+ const { paused } = body;
25
27
  if (typeof paused !== "boolean")
26
28
  return NextResponse.json(
27
29
  { error: "paused must be true or false" },
@@ -29,9 +31,10 @@ export async function POST(request: NextRequest, { params }: RouteParams) {
29
31
  );
30
32
  try {
31
33
  if (!paused)
32
- await verifyPresence(
33
- relyingParty(request.headers),
34
- assertion,
34
+ await requirePresence(
35
+ request,
36
+ gate.approver,
37
+ body,
35
38
  "resume",
36
39
  resumePresence(id)
37
40
  );
@@ -1,11 +1,11 @@
1
1
  import { NextRequest, NextResponse } from "next/server";
2
- import type { AuthenticationResponseJSON } from "@simplewebauthn/server";
2
+ import type { PresenceProof } from "@/lib/security/presence-http";
3
3
  import { removeRule, updateRule } from "@/lib/orchestrator/rules";
4
4
  import { provenAsSaad } from "../presence";
5
5
 
6
6
  type RouteParams = { params: Promise<{ id: string; ruleId: string }> };
7
7
 
8
- type Body = { text?: unknown; assertion?: AuthenticationResponseJSON };
8
+ type Body = { text?: unknown } & PresenceProof;
9
9
 
10
10
  // The rule, its body, and Saad's passkey on it; or the refusal.
11
11
  async function target(
@@ -21,7 +21,7 @@ async function target(
21
21
  response: NextResponse.json({ error: "Bad rule id" }, { status: 400 }),
22
22
  };
23
23
  const body = (await request.json().catch(() => ({}))) as Body;
24
- const refused = await provenAsSaad(request, id, body.assertion);
24
+ const refused = await provenAsSaad(request, id, body);
25
25
  if (refused) return { response: refused };
26
26
  return { workspaceId: id, ruleId: n, body };
27
27
  }
@@ -1,8 +1,10 @@
1
1
  import type { NextRequest } from "next/server";
2
- import type { AuthenticationResponseJSON } from "@simplewebauthn/server";
3
2
  import { requireApprover } from "@/lib/security/approver";
4
- import { relyingParty, verifyPresence } from "@/lib/security/presence";
5
- import { refusalResponse } from "@/lib/security/presence-http";
3
+ import {
4
+ refusalResponse,
5
+ requirePresence,
6
+ type PresenceProof,
7
+ } from "@/lib/security/presence-http";
6
8
  import { rulesPresence } from "@/lib/orchestrator/presence-binding";
7
9
 
8
10
  // A rule written here is Saad's, shown to the orchestrator as his word and
@@ -11,14 +13,15 @@ import { rulesPresence } from "@/lib/orchestrator/presence-binding";
11
13
  export async function provenAsSaad(
12
14
  request: NextRequest,
13
15
  workspaceId: string,
14
- assertion: AuthenticationResponseJSON | undefined
16
+ proof: PresenceProof | undefined
15
17
  ): Promise<Response | null> {
16
18
  const gate = requireApprover(request);
17
19
  if (!gate.ok) return gate.response;
18
20
  try {
19
- await verifyPresence(
20
- relyingParty(request.headers),
21
- assertion,
21
+ await requirePresence(
22
+ request,
23
+ gate.approver,
24
+ proof,
22
25
  "rules",
23
26
  rulesPresence(workspaceId)
24
27
  );
@@ -1,5 +1,5 @@
1
1
  import { NextRequest, NextResponse } from "next/server";
2
- import type { AuthenticationResponseJSON } from "@simplewebauthn/server";
2
+ import type { PresenceProof } from "@/lib/security/presence-http";
3
3
  import { getWorkspace } from "@/lib/workspaces";
4
4
  import { addRule, listRules } from "@/lib/orchestrator/rules";
5
5
  import { provenAsSaad } from "./presence";
@@ -19,13 +19,13 @@ export async function POST(request: NextRequest, { params }: RouteParams) {
19
19
  const { id } = await params;
20
20
  if (!getWorkspace(id))
21
21
  return NextResponse.json({ error: "Workspace not found" }, { status: 404 });
22
- const { text, assertion } = (await request.json().catch(() => ({}))) as {
22
+ const body = (await request.json().catch(() => ({}))) as {
23
23
  text?: unknown;
24
- assertion?: AuthenticationResponseJSON;
25
- };
24
+ } & PresenceProof;
25
+ const { text } = body;
26
26
  if (typeof text !== "string")
27
27
  return NextResponse.json({ error: "text is required" }, { status: 400 });
28
- const refused = await provenAsSaad(request, id, assertion);
28
+ const refused = await provenAsSaad(request, id, body);
29
29
  if (refused) return refused;
30
30
  try {
31
31
  return NextResponse.json({ rule: addRule(id, text, "saad") });
@@ -25,7 +25,8 @@ const when = (s: string) =>
25
25
  const fail = (e: Error) => toast.error(e.message);
26
26
 
27
27
  // Passkeys prove it's you approving an ask or resuming an orchestrator.
28
- // Each belongs to the host name it was made on.
28
+ // A browser passkey belongs to the host name it was made on; the app keeps its
29
+ // own key in the phone's Secure Enclave.
29
30
  export function PasskeysSection({ open }: { open: boolean }) {
30
31
  const { data } = usePasskeysQuery(open);
31
32
  const add = useAddPasskey();
@@ -64,7 +65,8 @@ export function PasskeysSection({ open }: { open: boolean }) {
64
65
  <div className="min-w-0 flex-1">
65
66
  <p className="truncate text-sm font-medium">{k.name}</p>
66
67
  <p className="text-muted-foreground truncate text-xs">
67
- {k.rp_id} · added {when(k.created_at)}
68
+ {k.type === "device" ? "Device key (Face ID)" : k.rp_id} · added{" "}
69
+ {when(k.created_at)}
68
70
  {k.registered_from ? ` from ${k.registered_from}` : ""} · used{" "}
69
71
  {lastSeen(k.last_used_at)}
70
72
  </p>
@@ -0,0 +1,90 @@
1
+ {
2
+ "1": "f6d5756a104c16ec",
3
+ "2": "3699f5f3cd215455",
4
+ "3": "dc4efbc29c2976ef",
5
+ "4": "d3db3af92f123a13",
6
+ "5": "d5937aba3b590fed",
7
+ "6": "0c4668cf9a58cecd",
8
+ "7": "7f535265ae1ad37f",
9
+ "8": "5c24f600f5f73145",
10
+ "9": "99496923fc7ff333",
11
+ "10": "22162670e68726c0",
12
+ "11": "21cd323c1654f9a9",
13
+ "12": "d66b982154dc3ee2",
14
+ "13": "ba1c8c4e2486a74d",
15
+ "14": "2f9575eed78d3284",
16
+ "15": "4c2293b3766c0d84",
17
+ "16": "8e798e52aed97875",
18
+ "17": "a743501f00312926",
19
+ "18": "cb84130b195bfa63",
20
+ "19": "b0ba5d304ca64aba",
21
+ "20": "ef3e125d28bfa506",
22
+ "21": "542664a9bef5f215",
23
+ "22": "195a6a7a87776269",
24
+ "23": "e4b50e60ac0821fa",
25
+ "24": "afb9644f31cc7602",
26
+ "25": "52bb47e1eed4e8d4",
27
+ "26": "9b5247315a01bdbe",
28
+ "27": "9c34c46d26c93cc4",
29
+ "28": "83d816f925aa0aba",
30
+ "29": "5ce556a828f7510b",
31
+ "30": "3d1bc245c30dcf49",
32
+ "31": "250433b280fd8df1",
33
+ "32": "95ec3c8125969681",
34
+ "33": "711226e6a027f6cd",
35
+ "34": "d8c917aa6f3e00b6",
36
+ "35": "7db35660583c2a50",
37
+ "36": "7ff8363149a3952c",
38
+ "37": "b7e72a18d3398da6",
39
+ "38": "cea02e00d1262601",
40
+ "39": "61c77d0f357d71d6",
41
+ "40": "6cedcf70268ddd25",
42
+ "41": "3d9af0548ae91946",
43
+ "42": "410ef4562a046ce6",
44
+ "43": "b0fca4e05ff6499e",
45
+ "44": "06b7f4dc636c9675",
46
+ "45": "73948f256c522959",
47
+ "46": "202f8fd102ef2581",
48
+ "47": "510435d6afa487cd",
49
+ "48": "10727869e8c8dea1",
50
+ "49": "0384a86395f6650a",
51
+ "50": "cc05c3663ca18b2b",
52
+ "51": "b381268fe096fb02",
53
+ "52": "281ad10453d9177a",
54
+ "53": "a7a53074ed49a566",
55
+ "54": "2358b106da3e7b68",
56
+ "55": "478ece72deb01953",
57
+ "56": "d04bafcaa2eda941",
58
+ "57": "0f9bc09a7a31f64d",
59
+ "58": "ccbf7eebccadfa3f",
60
+ "59": "1859ac87155d546b",
61
+ "60": "035e204a4b6c457e",
62
+ "61": "85d9bb65b051409a",
63
+ "62": "723d9c60994dec97",
64
+ "63": "77dd17570476ee17",
65
+ "64": "de5f368a88af93de",
66
+ "65": "081cc6eaf666581b",
67
+ "66": "08eedfefd3d3f3f9",
68
+ "67": "e37ead69c14e2d01",
69
+ "68": "9ab2551e258f1588",
70
+ "69": "55edb197ed55afe1",
71
+ "70": "d8a4faf39318bd1d",
72
+ "71": "45b9b9ff57d2083d",
73
+ "72": "51b77e2e1c5c8ae6",
74
+ "73": "2878b9f0762ab53d",
75
+ "74": "c94cd8f644a8518f",
76
+ "75": "e082428a046f9e6b",
77
+ "76": "17f3160630d658dc",
78
+ "77": "cd54bfd73fbcd168",
79
+ "78": "8ad44a982b1eb58d",
80
+ "79": "0b09d57d6ea8ce0a",
81
+ "80": "941d85f2ef8bd012",
82
+ "81": "ecd85de2bdf45ff1",
83
+ "82": "07f11cdac5f8444a",
84
+ "83": "eabf72b6a8616eac",
85
+ "84": "1ed4fb30b7435f28",
86
+ "85": "a85ac25c0e34f2fb",
87
+ "86": "50b5725a67286dee",
88
+ "87": "6024c62bcdbb186e",
89
+ "88": "bb58ec622ae398e5"
90
+ }