@saadnvd1/agent-os 0.3.548 → 0.3.549
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.
|
@@ -7,10 +7,12 @@ import { MAX_CLAIM_BYTES, readCapped } from "@/lib/security/read-capped";
|
|
|
7
7
|
import {
|
|
8
8
|
claimResponseBody,
|
|
9
9
|
claimSetsCookie,
|
|
10
|
+
isNativeClaim,
|
|
10
11
|
} from "@/lib/security/claim-response";
|
|
11
12
|
|
|
12
13
|
// POST /api/pair/claim - reachable without a token; trades a code for one.
|
|
13
|
-
// A client that isn't a browser sends `token: true` to get it in the body too
|
|
14
|
+
// A client that isn't a browser sends `token: true` to get it in the body too,
|
|
15
|
+
// and starts with Can approve: it held a one-time code, as a browser does.
|
|
14
16
|
export async function POST(request: NextRequest) {
|
|
15
17
|
const raw = await readCapped(request, MAX_CLAIM_BYTES);
|
|
16
18
|
if (raw === null)
|
|
@@ -23,6 +25,10 @@ export async function POST(request: NextRequest) {
|
|
|
23
25
|
}
|
|
24
26
|
if (!body.code)
|
|
25
27
|
return NextResponse.json({ error: "code required" }, { status: 400 });
|
|
28
|
+
const native = isNativeClaim({
|
|
29
|
+
wantsToken: body.token,
|
|
30
|
+
origin: request.headers.get("origin"),
|
|
31
|
+
});
|
|
26
32
|
const result = claimPairing({
|
|
27
33
|
code: body.code,
|
|
28
34
|
name: body.name ?? "",
|
|
@@ -31,6 +37,7 @@ export async function POST(request: NextRequest) {
|
|
|
31
37
|
request.headers.get(REMOTE_HEADER),
|
|
32
38
|
request.headers.get("x-forwarded-for")
|
|
33
39
|
),
|
|
40
|
+
native,
|
|
34
41
|
});
|
|
35
42
|
if (!result.ok) {
|
|
36
43
|
const status = result.error === "rate_limited" ? 429 : 400;
|
package/lib/db/migrations.ts
CHANGED
|
@@ -1994,6 +1994,22 @@ const migrations: Migration[] = [
|
|
|
1994
1994
|
);
|
|
1995
1995
|
},
|
|
1996
1996
|
},
|
|
1997
|
+
{
|
|
1998
|
+
id: 83,
|
|
1999
|
+
name: "native_devices_can_approve",
|
|
2000
|
+
up: (db) => {
|
|
2001
|
+
// New native pairings start with Can approve; this gives it once to the
|
|
2002
|
+
// ones paired before. The claim wasn't recorded, so the app is known by
|
|
2003
|
+
// its user agent (`AgentOS/<build> CFNetwork/...` on iOS) or by an
|
|
2004
|
+
// app push token. It only ever turns the flag on.
|
|
2005
|
+
db.exec(
|
|
2006
|
+
`UPDATE devices SET can_approve = 1
|
|
2007
|
+
WHERE revoked_at IS NULL AND can_approve = 0
|
|
2008
|
+
AND (user_agent LIKE 'AgentOS/% CFNetwork/%'
|
|
2009
|
+
OR push_platform IN ('ios', 'android'))`
|
|
2010
|
+
);
|
|
2011
|
+
},
|
|
2012
|
+
},
|
|
1997
2013
|
];
|
|
1998
2014
|
|
|
1999
2015
|
// `upTo`: stop after this id (tests that start from an older database).
|
package/lib/security/approver.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Who may answer the orchestrator's asks or pause it: this machine, the
|
|
3
|
-
* tailnet, or a paired device
|
|
3
|
+
* tailnet, or a paired device with Can approve (the native app gets it at
|
|
4
|
+
* pairing; Saad can turn it off or on in Devices). This
|
|
4
5
|
* is the coarse gate; approvals that matter also need a passkey (presence).
|
|
5
6
|
*/
|
|
6
7
|
|
|
@@ -11,13 +11,19 @@ export interface ClaimBody {
|
|
|
11
11
|
token?: string;
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
+
// The native app asks for its token and sends no Origin; a browser always
|
|
15
|
+
// sends one on a POST, so a page can't pass for the app.
|
|
16
|
+
export const isNativeClaim = (request: {
|
|
17
|
+
wantsToken?: unknown;
|
|
18
|
+
origin: string | null;
|
|
19
|
+
}) => request.wantsToken === true && !request.origin;
|
|
20
|
+
|
|
14
21
|
export function claimResponseBody(
|
|
15
22
|
device: { id: string; name: string },
|
|
16
23
|
token: string,
|
|
17
24
|
request: { wantsToken?: unknown; origin: string | null }
|
|
18
25
|
): ClaimBody {
|
|
19
|
-
|
|
20
|
-
return { device, ...(native ? { token } : {}) };
|
|
26
|
+
return { device, ...(isNativeClaim(request) ? { token } : {}) };
|
|
21
27
|
}
|
|
22
28
|
|
|
23
29
|
// Whether the reply also sets the device cookie: only for a browser. A
|
package/lib/security/devices.ts
CHANGED
|
@@ -20,7 +20,8 @@ export interface Device {
|
|
|
20
20
|
last_seen_at: string | null;
|
|
21
21
|
last_address: string | null;
|
|
22
22
|
revoked_at: string | null;
|
|
23
|
-
//
|
|
23
|
+
// It may answer the orchestrator's asks: on for the native app at
|
|
24
|
+
// pairing, off for a browser; Saad can flip it either way in Devices.
|
|
24
25
|
can_approve: number;
|
|
25
26
|
}
|
|
26
27
|
|
|
@@ -32,19 +33,22 @@ export const hashToken = (token: string) =>
|
|
|
32
33
|
|
|
33
34
|
export function mintDevice(
|
|
34
35
|
name: string,
|
|
35
|
-
userAgent?: string | null
|
|
36
|
+
userAgent?: string | null,
|
|
37
|
+
canApprove = false
|
|
36
38
|
): { device: Device; token: string } {
|
|
37
39
|
const id = crypto.randomUUID();
|
|
38
40
|
const token = TOKEN_PREFIX + crypto.randomBytes(32).toString("base64url");
|
|
39
41
|
getDb()
|
|
40
42
|
.prepare(
|
|
41
|
-
`INSERT INTO devices (id, name, token_hash, user_agent
|
|
43
|
+
`INSERT INTO devices (id, name, token_hash, user_agent, can_approve)
|
|
44
|
+
VALUES (?, ?, ?, ?, ?)`
|
|
42
45
|
)
|
|
43
46
|
.run(
|
|
44
47
|
id,
|
|
45
48
|
name.trim().slice(0, 80) || "Device",
|
|
46
49
|
hashToken(token),
|
|
47
|
-
userAgent ?? null
|
|
50
|
+
userAgent ?? null,
|
|
51
|
+
canApprove ? 1 : 0
|
|
48
52
|
);
|
|
49
53
|
return { device: getDevice(id)!, token };
|
|
50
54
|
}
|
package/lib/security/pairing.ts
CHANGED
|
@@ -82,6 +82,8 @@ export function claimPairing(
|
|
|
82
82
|
name: string;
|
|
83
83
|
userAgent?: string | null;
|
|
84
84
|
address: string;
|
|
85
|
+
// The native app (claimResponseBody's test): it may approve from the start.
|
|
86
|
+
native?: boolean;
|
|
85
87
|
},
|
|
86
88
|
now = Date.now()
|
|
87
89
|
): ClaimResult {
|
|
@@ -103,7 +105,11 @@ export function claimPairing(
|
|
|
103
105
|
const p = state.codes.get(normalizeCode(input.code));
|
|
104
106
|
if (!p || p.claimed || p.expiresAt < now)
|
|
105
107
|
return { ok: false, error: "invalid" };
|
|
106
|
-
const { device, token } = mintDevice(
|
|
108
|
+
const { device, token } = mintDevice(
|
|
109
|
+
input.name,
|
|
110
|
+
input.userAgent,
|
|
111
|
+
input.native === true
|
|
112
|
+
);
|
|
107
113
|
p.claimed = { deviceId: device.id, name: device.name };
|
|
108
114
|
return { ok: true, device, token };
|
|
109
115
|
}
|