Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/device-approval-context.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@objectstack/cloud-connection": patch
---

bind/start appends device context (`runtime_name`, `runtime_version`) to the device-flow verification URLs so the cloud approval page can show WHAT is being authorized (ADR runtime-identity-binding §2.3). Display-only informed-consent context; the approval page pairs it with an "only approve if you started this" warning.
20 changes: 18 additions & 2 deletions packages/cloud-connection/src/cloud-connection-plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -271,11 +271,27 @@ export class CloudConnectionPlugin implements Plugin {
});
const json: any = await resp.json().catch(() => ({}));
if (!resp.ok) return c.json({ success: false, error: { code: 'device_code_failed', message: json?.error ?? `device/code ${resp.status}` } }, 502);
// Device context for the approval page (ADR
// runtime-identity-binding §2.3): name the requesting
// runtime in the verification URL so the approver sees
// WHAT they are authorizing. Display-only — informed
// consent, not an authenticity proof (the page carries
// the "only approve if you started this" warning).
const withContext = (uri: unknown): string | undefined => {
if (typeof uri !== 'string' || !uri) return undefined;
try {
const u = new URL(uri);
try { u.searchParams.set('runtime_name', hostname()); } catch { /* no hostname */ }
const ver = (process.env.OS_RUNTIME_VERSION ?? this.version) || '';
if (ver) u.searchParams.set('runtime_version', ver);
return u.toString();
} catch { return uri; }
};
return c.json({ success: true, data: {
device_code: json.device_code,
user_code: json.user_code,
verification_uri: json.verification_uri,
verification_uri_complete: json.verification_uri_complete,
verification_uri: withContext(json.verification_uri),
verification_uri_complete: withContext(json.verification_uri_complete),
interval: json.interval ?? 5,
expires_in: json.expires_in ?? 600,
} });
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,11 @@ describe('CloudConnectionPlugin credential behavior', () => {
);
expect(res.status).toBe(200);
expect(res.payload.data.user_code).toBe('ABCD-EFGH');
// Device context rides the verification URL for the approval page.
const complete = new URL(res.payload.data.verification_uri_complete);
expect(complete.searchParams.get('user_code')).toBe('ABCD-EFGH');
expect(complete.searchParams.get('runtime_name')).toBeTruthy();
expect(complete.searchParams.get('runtime_version')).toBeTruthy();
});

it('unbind without an environment id revokes via the bearer (empty body) and clears the store', async () => {
Expand Down