@volter/twin-googleoauth 0.1.1 → 0.1.2

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.
@@ -11,7 +11,7 @@
11
11
  // `node:crypto` is required LAZILY (inside the functions that use it), NOT as a top-level static
12
12
  // import, so this module can be pulled into the browser bundle of the consent client without Bun's
13
13
  // browser target choking on it — the signing helpers are server-only and never reached there.
14
- import { applyTwinWrite, projectResources, nodeBuiltin } from '@volter/world-core';
14
+ import { applyTwinWrite, projectResources, nodeBuiltin, runAsVendorMove } from '@volter/world-core';
15
15
  function nodeCrypto() {
16
16
  // eslint-disable-next-line @typescript-eslint/no-require-imports
17
17
  return nodeBuiltin('node:crypto');
@@ -51,13 +51,14 @@ export async function ensureKeypair(root) {
51
51
  }
52
52
  const { publicKeyPem, privateKeyPem } = keypairGenerator.current();
53
53
  const kid = kidFor(publicKeyPem);
54
- await applyTwinWrite(SERVICE, {
54
+ // Google's own signing key: a vendor move, made on first use whoever asks (a read-only request too)
55
+ await runAsVendorMove(() => applyTwinWrite(SERVICE, {
55
56
  operation: '_keypair.create',
56
57
  subjectType: KEYPAIR_TYPE,
57
58
  subjectId: KEYPAIR_ID,
58
59
  fields: { _kid: kid, _publicKeyPem: publicKeyPem, _privateKeyPem: privateKeyPem },
59
60
  actor: { kind: 'system' },
60
- }, root);
61
+ }, root));
61
62
  return { kid, publicKeyPem, privateKeyPem };
62
63
  }
63
64
  /** Stable key id = first 40 hex of SHA-1-length SHA-256 over the SPKI DER. Google's own kids are
@@ -13,7 +13,7 @@
13
13
  // It also tells the handler its OWN origin, so the OIDC discovery document and the consent form's
14
14
  // action point BACK AT THE TWIN. Without that a discovery-driven client (openid-client) would read
15
15
  // the document and walk straight out to the real accounts.google.com.
16
- import { serveHttp } from '@volter/world-core';
16
+ import { isReadOnlyRequest, serveHttp, withRequestScopes } from '@volter/world-core';
17
17
  import { CONSENT_CLIENT_CSS, CONSENT_CLIENT_JS, CONSENT_SCRIPT_PATH, CONSENT_STYLE_PATH } from "./googleoauth-consent-ui.js";
18
18
  import { worldNow, statefulTwinManifest, twinPublicBase } from '@volter/world-core';
19
19
  import { handleGoogleOAuthTwinRequest } from "./googleoauth-twin.js";
@@ -31,8 +31,10 @@ import { handleGoogleOAuthTwinRequest } from "./googleoauth-twin.js";
31
31
  * plane by `scripts/consent-clients.ts`) — no build and no disk read on the fetch path.
32
32
  */
33
33
  export function createGoogleOAuthTwinFetch(options = {}) {
34
- const readOnly = options.readOnly ?? false;
35
- return async function googleOAuthTwinFetch(request) {
34
+ // a read-only request (x-volter-read-only) is a request to a read-only twin: the flow's writes (a code,
35
+ // a token, a revoke) are refused as Google-shaped errors, and the kernel refuses any that slip past
36
+ return withRequestScopes(async function googleOAuthTwinFetch(request) {
37
+ const readOnly = (options.readOnly ?? false) || isReadOnlyRequest(request);
36
38
  const url = new URL(request.url);
37
39
  // GET /twin — the discovery manifest (education inside the twin).
38
40
  if (request.method === 'GET' && url.pathname.replace(/\/+$/, '') === '/twin') {
@@ -70,7 +72,7 @@ export function createGoogleOAuthTwinFetch(options = {}) {
70
72
  }
71
73
  out['content-type'] = out['content-type'] ?? 'application/json; charset=utf-8';
72
74
  return new Response(JSON.stringify(res.body), { status: res.status, headers: out });
73
- };
75
+ }, { refuse: () => Response.json({ error: 'temporarily_unavailable', error_description: 'this request is read-only' }, { status: 405 }) });
74
76
  }
75
77
  export async function createGoogleOAuthTwinServer(options) {
76
78
  const server = await serveHttp({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@volter/twin-googleoauth",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "description": "Local Google OAuth 2.0 / OpenID Connect twin — the real consent screen, the real authorization-code round trip, and real RS256 id_tokens an unmodified OAuth client library completes a full flow against. Built on @volter/world-core.",
5
5
  "keywords": [
6
6
  "twin",
@@ -57,10 +57,10 @@
57
57
  "react-dom": "^19.2.7"
58
58
  },
59
59
  "peerDependencies": {
60
- "@volter/world-core": "2.0.1"
60
+ "@volter/world-core": "2.0.2"
61
61
  },
62
62
  "devDependencies": {
63
- "@volter/world-core": "2.0.1",
63
+ "@volter/world-core": "2.0.2",
64
64
  "@volter/world-tooling": "0.1.0",
65
65
  "google-auth-library": "^9.15.1",
66
66
  "@types/bun": "^1.2.20",
@@ -11,7 +11,7 @@
11
11
  // `node:crypto` is required LAZILY (inside the functions that use it), NOT as a top-level static
12
12
  // import, so this module can be pulled into the browser bundle of the consent client without Bun's
13
13
  // browser target choking on it — the signing helpers are server-only and never reached there.
14
- import { applyTwinWrite, projectResources, nodeBuiltin } from '@volter/world-core';
14
+ import { applyTwinWrite, projectResources, nodeBuiltin, runAsVendorMove } from '@volter/world-core';
15
15
 
16
16
  type NodeCrypto = typeof import('node:crypto');
17
17
  function nodeCrypto(): NodeCrypto {
@@ -62,7 +62,8 @@ export async function ensureKeypair(root?: string): Promise<GoogleOAuthKeypair>
62
62
  }
63
63
  const { publicKeyPem, privateKeyPem } = keypairGenerator.current();
64
64
  const kid = kidFor(publicKeyPem);
65
- await applyTwinWrite(
65
+ // Google's own signing key: a vendor move, made on first use whoever asks (a read-only request too)
66
+ await runAsVendorMove(() => applyTwinWrite(
66
67
  SERVICE,
67
68
  {
68
69
  operation: '_keypair.create',
@@ -72,7 +73,7 @@ export async function ensureKeypair(root?: string): Promise<GoogleOAuthKeypair>
72
73
  actor: { kind: 'system' },
73
74
  },
74
75
  root,
75
- );
76
+ ));
76
77
  return { kid, publicKeyPem, privateKeyPem };
77
78
  }
78
79
 
@@ -13,7 +13,7 @@
13
13
  // It also tells the handler its OWN origin, so the OIDC discovery document and the consent form's
14
14
  // action point BACK AT THE TWIN. Without that a discovery-driven client (openid-client) would read
15
15
  // the document and walk straight out to the real accounts.google.com.
16
- import { serveHttp } from '@volter/world-core';
16
+ import { isReadOnlyRequest, serveHttp, withRequestScopes } from '@volter/world-core';
17
17
  import { CONSENT_CLIENT_CSS, CONSENT_CLIENT_JS, CONSENT_SCRIPT_PATH, CONSENT_STYLE_PATH } from './googleoauth-consent-ui.ts';
18
18
  import { worldNow, statefulTwinManifest, twinPublicBase } from '@volter/world-core';
19
19
  import { handleGoogleOAuthTwinRequest } from './googleoauth-twin.ts';
@@ -38,8 +38,10 @@ export interface GoogleOAuthTwinFetchOptions {
38
38
  * plane by `scripts/consent-clients.ts`) — no build and no disk read on the fetch path.
39
39
  */
40
40
  export function createGoogleOAuthTwinFetch(options: GoogleOAuthTwinFetchOptions = {}): (request: Request) => Promise<Response> {
41
- const readOnly = options.readOnly ?? false;
42
- return async function googleOAuthTwinFetch(request: Request): Promise<Response> {
41
+ // a read-only request (x-volter-read-only) is a request to a read-only twin: the flow's writes (a code,
42
+ // a token, a revoke) are refused as Google-shaped errors, and the kernel refuses any that slip past
43
+ return withRequestScopes(async function googleOAuthTwinFetch(request: Request): Promise<Response> {
44
+ const readOnly = (options.readOnly ?? false) || isReadOnlyRequest(request);
43
45
  const url = new URL(request.url);
44
46
  // GET /twin — the discovery manifest (education inside the twin).
45
47
  if (request.method === 'GET' && url.pathname.replace(/\/+$/, '') === '/twin') {
@@ -80,7 +82,7 @@ export function createGoogleOAuthTwinFetch(options: GoogleOAuthTwinFetchOptions
80
82
  }
81
83
  out['content-type'] = out['content-type'] ?? 'application/json; charset=utf-8';
82
84
  return new Response(JSON.stringify(res.body), { status: res.status, headers: out });
83
- };
85
+ }, { refuse: () => Response.json({ error: 'temporarily_unavailable', error_description: 'this request is read-only' }, { status: 405 }) });
84
86
  }
85
87
 
86
88
  export async function createGoogleOAuthTwinServer(options: { root?: string; port?: number; readOnly?: boolean }): Promise<{ port: number; stop: () => void }> {