-
Notifications
You must be signed in to change notification settings - Fork 6
Expand file tree
/
Copy pathsys-device-code.object.ts
More file actions
146 lines (130 loc) · 4.76 KB
/
Copy pathsys-device-code.object.ts
File metadata and controls
146 lines (130 loc) · 4.76 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
import { ObjectSchema, Field } from '@objectstack/spec/data';
/**
* sys_device_code — System Device Authorization Code Object
*
* Stores pending RFC 8628 OAuth Device Authorization Grant requests.
* Backed by better-auth's `device-authorization` plugin (`deviceCode` model).
*
* Lifecycle:
* 1. CLI calls `POST /device/code` → row inserted with status='pending'
* 2. Browser visits `verification_uri_complete` and the signed-in user
* calls `POST /device/approve` (or `/device/deny`) → status flips
* 3. CLI's next `POST /device/token` poll either receives a session token
* (status=approved) or one of the standard error codes
* (`authorization_pending`, `slow_down`, `expired_token`,
* `access_denied`). Approved rows are deleted on token issuance.
*
* @namespace sys
*/
export const SysDeviceCode = ObjectSchema.create({
name: 'sys_device_code',
label: 'Device Code',
pluralLabel: 'Device Codes',
icon: 'key-round',
isSystem: true,
managedBy: 'better-auth',
// ADR-0057: device codes are dead the moment `expires_at` passes — keep a
// 1d grace for post-mortem, then reap.
lifecycle: {
class: 'transient',
ttl: { field: 'expires_at', expireAfter: '1d' },
},
// [ADR-0066 D2/④] Secure-by-default: rows are LIVE pending device-grant
// codes — reading `user_code`/`device_code` lets an attacker hijack a
// pending CLI login. Not covered by the wildcard `'*'` grant; admins retain
// access via the superuser bypass; better-auth reads via its adapter
// (system context), so the device-grant flow is unaffected.
access: { default: 'private' },
// ADR-0010 §3.7 — managed by better-auth; tenants may not edit schema,
// but may add overlay row-level config. Use `no-overlay` if you need to
// forbid sys_metadata overlays entirely.
protection: {
lock: 'full',
reason: 'Identity table managed by better-auth — see ADR-0010.',
docsUrl: 'https://docs.objectstack.ai/adr/0010-metadata-protection',
},
description: 'OAuth 2.0 Device Authorization Grant (RFC 8628) pending requests',
nameField: 'user_code', // [ADR-0079] canonical primary-title pointer (single-field titleFormat)
titleFormat: '{user_code}',
highlightFields: ['user_code', 'status', 'client_id', 'expires_at'],
fields: {
id: Field.text({
label: 'Device Code ID',
required: true,
readonly: true,
}),
created_at: Field.datetime({
label: 'Created At',
defaultValue: 'NOW()',
readonly: true,
}),
updated_at: Field.datetime({
label: 'Updated At',
defaultValue: 'NOW()',
readonly: true,
}),
/** High-entropy token returned to the device (CLI). Polled at /device/token. */
device_code: Field.text({
label: 'Device Code',
required: true,
description: 'High-entropy token returned to the polling device',
}),
/** Human-readable short code displayed to the user (e.g. ABCD-EFGH). */
user_code: Field.text({
label: 'User Code',
required: true,
description: 'Short user-facing code (e.g. ABCD-EFGH)',
}),
/** Owning user — populated when the request is approved. */
user_id: Field.lookup('sys_user', {
label: 'User',
required: false,
description: 'User who approved the device authorization',
}),
expires_at: Field.datetime({
label: 'Expires At',
required: true,
description: 'When the device & user codes are no longer valid',
}),
/** 'pending' | 'approved' | 'denied' */
status: Field.text({
label: 'Status',
required: true,
description: "Current status: 'pending' | 'approved' | 'denied'",
}),
last_polled_at: Field.datetime({
label: 'Last Polled At',
required: false,
description: 'Timestamp of the most recent /device/token poll',
}),
polling_interval: Field.number({
label: 'Polling Interval (ms)',
required: false,
description: 'Server-recommended minimum polling interval, in ms',
}),
client_id: Field.text({
label: 'Client ID',
required: false,
description: 'OAuth client identifier of the requesting device',
}),
scope: Field.text({
label: 'Scope',
required: false,
description: 'Space-separated OAuth scopes requested by the device',
}),
},
indexes: [
{ fields: ['device_code'], unique: true },
{ fields: ['user_code'], unique: true },
{ fields: ['status'], unique: false },
],
enable: {
trackHistory: false,
searchable: false,
apiEnabled: true,
// #1591 — reads only: writes are refused by the identity write guard
// (ADR-0092 D2) and owned by better-auth. HTTP answers 405 before the 403.
apiMethods: ['get'],
},
});