unotoken 0.1.0

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.
Files changed (122) hide show
  1. package/README.md +360 -0
  2. package/dist/cli.d.ts +17 -0
  3. package/dist/cli.d.ts.map +1 -0
  4. package/dist/cli.js +1207 -0
  5. package/dist/cli.js.map +1 -0
  6. package/dist/client.d.ts +15 -0
  7. package/dist/client.d.ts.map +1 -0
  8. package/dist/client.js +15 -0
  9. package/dist/client.js.map +1 -0
  10. package/dist/db.d.ts +52 -0
  11. package/dist/db.d.ts.map +1 -0
  12. package/dist/db.js +97 -0
  13. package/dist/db.js.map +1 -0
  14. package/dist/dotenv.d.ts +69 -0
  15. package/dist/dotenv.d.ts.map +1 -0
  16. package/dist/dotenv.js +115 -0
  17. package/dist/dotenv.js.map +1 -0
  18. package/dist/env-mapper.d.ts +55 -0
  19. package/dist/env-mapper.d.ts.map +1 -0
  20. package/dist/env-mapper.js +97 -0
  21. package/dist/env-mapper.js.map +1 -0
  22. package/dist/exec.d.ts +80 -0
  23. package/dist/exec.d.ts.map +1 -0
  24. package/dist/exec.js +214 -0
  25. package/dist/exec.js.map +1 -0
  26. package/dist/index.d.ts +12 -0
  27. package/dist/index.d.ts.map +1 -0
  28. package/dist/index.js +43 -0
  29. package/dist/index.js.map +1 -0
  30. package/dist/oauth/commands.d.ts +151 -0
  31. package/dist/oauth/commands.d.ts.map +1 -0
  32. package/dist/oauth/commands.js +322 -0
  33. package/dist/oauth/commands.js.map +1 -0
  34. package/dist/oauth/config.d.ts +84 -0
  35. package/dist/oauth/config.d.ts.map +1 -0
  36. package/dist/oauth/config.js +156 -0
  37. package/dist/oauth/config.js.map +1 -0
  38. package/dist/oauth/crypto-helpers.d.ts +44 -0
  39. package/dist/oauth/crypto-helpers.d.ts.map +1 -0
  40. package/dist/oauth/crypto-helpers.js +94 -0
  41. package/dist/oauth/crypto-helpers.js.map +1 -0
  42. package/dist/oauth/device-secret.d.ts +57 -0
  43. package/dist/oauth/device-secret.d.ts.map +1 -0
  44. package/dist/oauth/device-secret.js +106 -0
  45. package/dist/oauth/device-secret.js.map +1 -0
  46. package/dist/oauth/flow.d.ts +112 -0
  47. package/dist/oauth/flow.d.ts.map +1 -0
  48. package/dist/oauth/flow.js +255 -0
  49. package/dist/oauth/flow.js.map +1 -0
  50. package/dist/oauth/index.d.ts +18 -0
  51. package/dist/oauth/index.d.ts.map +1 -0
  52. package/dist/oauth/index.js +24 -0
  53. package/dist/oauth/index.js.map +1 -0
  54. package/dist/oauth/key-wrap.d.ts +146 -0
  55. package/dist/oauth/key-wrap.d.ts.map +1 -0
  56. package/dist/oauth/key-wrap.js +275 -0
  57. package/dist/oauth/key-wrap.js.map +1 -0
  58. package/dist/oauth/pkce.d.ts +29 -0
  59. package/dist/oauth/pkce.d.ts.map +1 -0
  60. package/dist/oauth/pkce.js +34 -0
  61. package/dist/oauth/pkce.js.map +1 -0
  62. package/dist/oauth/provider.d.ts +79 -0
  63. package/dist/oauth/provider.d.ts.map +1 -0
  64. package/dist/oauth/provider.js +10 -0
  65. package/dist/oauth/provider.js.map +1 -0
  66. package/dist/oauth/providers/github.d.ts +75 -0
  67. package/dist/oauth/providers/github.d.ts.map +1 -0
  68. package/dist/oauth/providers/github.js +119 -0
  69. package/dist/oauth/providers/github.js.map +1 -0
  70. package/dist/oauth/providers/google.d.ts +115 -0
  71. package/dist/oauth/providers/google.d.ts.map +1 -0
  72. package/dist/oauth/providers/google.js +285 -0
  73. package/dist/oauth/providers/google.js.map +1 -0
  74. package/dist/sdk.d.ts +8 -0
  75. package/dist/sdk.d.ts.map +1 -0
  76. package/dist/sdk.js +8 -0
  77. package/dist/sdk.js.map +1 -0
  78. package/dist/server.d.ts +33 -0
  79. package/dist/server.d.ts.map +1 -0
  80. package/dist/server.js +287 -0
  81. package/dist/server.js.map +1 -0
  82. package/dist/signatures/approval-codes.d.ts +192 -0
  83. package/dist/signatures/approval-codes.d.ts.map +1 -0
  84. package/dist/signatures/approval-codes.js +407 -0
  85. package/dist/signatures/approval-codes.js.map +1 -0
  86. package/dist/signatures/commands.d.ts +108 -0
  87. package/dist/signatures/commands.d.ts.map +1 -0
  88. package/dist/signatures/commands.js +270 -0
  89. package/dist/signatures/commands.js.map +1 -0
  90. package/dist/signatures/devices.d.ts +165 -0
  91. package/dist/signatures/devices.d.ts.map +1 -0
  92. package/dist/signatures/devices.js +344 -0
  93. package/dist/signatures/devices.js.map +1 -0
  94. package/dist/signatures/email-config.d.ts +102 -0
  95. package/dist/signatures/email-config.d.ts.map +1 -0
  96. package/dist/signatures/email-config.js +188 -0
  97. package/dist/signatures/email-config.js.map +1 -0
  98. package/dist/signatures/email.d.ts +106 -0
  99. package/dist/signatures/email.d.ts.map +1 -0
  100. package/dist/signatures/email.js +180 -0
  101. package/dist/signatures/email.js.map +1 -0
  102. package/dist/signatures/fingerprint.d.ts +70 -0
  103. package/dist/signatures/fingerprint.d.ts.map +1 -0
  104. package/dist/signatures/fingerprint.js +123 -0
  105. package/dist/signatures/fingerprint.js.map +1 -0
  106. package/dist/signatures/guard.d.ts +118 -0
  107. package/dist/signatures/guard.d.ts.map +1 -0
  108. package/dist/signatures/guard.js +310 -0
  109. package/dist/signatures/guard.js.map +1 -0
  110. package/dist/signatures/resend.d.ts +84 -0
  111. package/dist/signatures/resend.d.ts.map +1 -0
  112. package/dist/signatures/resend.js +248 -0
  113. package/dist/signatures/resend.js.map +1 -0
  114. package/dist/token-requests.d.ts +80 -0
  115. package/dist/token-requests.d.ts.map +1 -0
  116. package/dist/token-requests.js +201 -0
  117. package/dist/token-requests.js.map +1 -0
  118. package/dist/tokens.d.ts +80 -0
  119. package/dist/tokens.d.ts.map +1 -0
  120. package/dist/tokens.js +150 -0
  121. package/dist/tokens.js.map +1 -0
  122. package/package.json +62 -0
@@ -0,0 +1,18 @@
1
+ /**
2
+ * OAuth module for unotoken.
3
+ *
4
+ * Provides a provider-agnostic PKCE browser flow engine
5
+ * for authenticating users via OAuth/OIDC providers,
6
+ * plus key wrapping for OAuth-derived vault unlock.
7
+ */
8
+ export { generatePKCE, computeChallenge, type PKCEPair } from './pkce.js';
9
+ export type { OAuthProvider, OAuthIdentity, TokenResponse, } from './provider.js';
10
+ export { runOAuthFlow, startCallbackServer, openSystemBrowser, exchangeCode, OAuthTimeoutError, OAuthCallbackError, OAuthTokenError, type OAuthFlowConfig, type OAuthFlowResult, } from './flow.js';
11
+ export { wrapMasterKey, unwrapMasterKey, listOAuthLinks, removeOAuthLink, initOAuthLinksTable, deriveWrapKey, encryptMasterKey, decryptMasterKey, KeyUnwrapError, WRAP_KEY_BYTES, WRAP_SALT_BYTES, WRAP_NONCE_BYTES, type OAuthLinkRow, type OAuthLinksDb, } from './key-wrap.js';
12
+ export { generateDeviceSecret, readDeviceSecret, deviceSecretExists, getDeviceKeyPath, getDeviceDir, DeviceSecretMissingError, DEVICE_SECRET_BYTES, } from './device-secret.js';
13
+ export { createGoogleProvider, clearGoogleCaches, type GoogleProviderOptions, type GoogleDiscoveryDocument, type GoogleIdTokenClaims, type JWK, type JWKSResponse, } from './providers/google.js';
14
+ export { createGitHubProvider, type GitHubProviderOptions, type GitHubUser, } from './providers/github.js';
15
+ export { deriveMasterKey, decryptWithKey, verifyMasterKey, SALT_BYTES, KEY_BYTES, NONCE_BYTES, } from './crypto-helpers.js';
16
+ export { authLink, unlockWithOAuth, listLinkedProviders, unlinkProvider, isValidProvider, resolveProvider, MultipleProvidersError, type ProviderName, type AuthLinkOptions, type AuthLinkResult, type UnlockOAuthOptions, type UnlockOAuthResult, type UnlinkResult, type LinkedProvider, } from './commands.js';
17
+ export { loadOAuthConfig, saveOAuthConfig, resetOAuthConfig, updateOAuthConfig, hasCustomConfig, formatConfigForDisplay, maskSecret, getConfigPath, type OAuthConfigFile, } from './config.js';
18
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/oauth/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,YAAY,EAAE,gBAAgB,EAAE,KAAK,QAAQ,EAAE,MAAM,WAAW,CAAC;AAE1E,YAAY,EACV,aAAa,EACb,aAAa,EACb,aAAa,GACd,MAAM,eAAe,CAAC;AAEvB,OAAO,EACL,YAAY,EACZ,mBAAmB,EACnB,iBAAiB,EACjB,YAAY,EACZ,iBAAiB,EACjB,kBAAkB,EAClB,eAAe,EACf,KAAK,eAAe,EACpB,KAAK,eAAe,GACrB,MAAM,WAAW,CAAC;AAGnB,OAAO,EACL,aAAa,EACb,eAAe,EACf,cAAc,EACd,eAAe,EACf,mBAAmB,EACnB,aAAa,EACb,gBAAgB,EAChB,gBAAgB,EAChB,cAAc,EACd,cAAc,EACd,eAAe,EACf,gBAAgB,EAChB,KAAK,YAAY,EACjB,KAAK,YAAY,GAClB,MAAM,eAAe,CAAC;AAGvB,OAAO,EACL,oBAAoB,EACpB,gBAAgB,EAChB,kBAAkB,EAClB,gBAAgB,EAChB,YAAY,EACZ,wBAAwB,EACxB,mBAAmB,GACpB,MAAM,oBAAoB,CAAC;AAG5B,OAAO,EACL,oBAAoB,EACpB,iBAAiB,EACjB,KAAK,qBAAqB,EAC1B,KAAK,uBAAuB,EAC5B,KAAK,mBAAmB,EACxB,KAAK,GAAG,EACR,KAAK,YAAY,GAClB,MAAM,uBAAuB,CAAC;AAG/B,OAAO,EACL,oBAAoB,EACpB,KAAK,qBAAqB,EAC1B,KAAK,UAAU,GAChB,MAAM,uBAAuB,CAAC;AAG/B,OAAO,EACL,eAAe,EACf,cAAc,EACd,eAAe,EACf,UAAU,EACV,SAAS,EACT,WAAW,GACZ,MAAM,qBAAqB,CAAC;AAG7B,OAAO,EACL,QAAQ,EACR,eAAe,EACf,mBAAmB,EACnB,cAAc,EACd,eAAe,EACf,eAAe,EACf,sBAAsB,EACtB,KAAK,YAAY,EACjB,KAAK,eAAe,EACpB,KAAK,cAAc,EACnB,KAAK,kBAAkB,EACvB,KAAK,iBAAiB,EACtB,KAAK,YAAY,EACjB,KAAK,cAAc,GACpB,MAAM,eAAe,CAAC;AAGvB,OAAO,EACL,eAAe,EACf,eAAe,EACf,gBAAgB,EAChB,iBAAiB,EACjB,eAAe,EACf,sBAAsB,EACtB,UAAU,EACV,aAAa,EACb,KAAK,eAAe,GACrB,MAAM,aAAa,CAAC"}
@@ -0,0 +1,24 @@
1
+ /**
2
+ * OAuth module for unotoken.
3
+ *
4
+ * Provides a provider-agnostic PKCE browser flow engine
5
+ * for authenticating users via OAuth/OIDC providers,
6
+ * plus key wrapping for OAuth-derived vault unlock.
7
+ */
8
+ export { generatePKCE, computeChallenge } from './pkce.js';
9
+ export { runOAuthFlow, startCallbackServer, openSystemBrowser, exchangeCode, OAuthTimeoutError, OAuthCallbackError, OAuthTokenError, } from './flow.js';
10
+ // Key wrapping for OAuth-derived unlock (US-003)
11
+ export { wrapMasterKey, unwrapMasterKey, listOAuthLinks, removeOAuthLink, initOAuthLinksTable, deriveWrapKey, encryptMasterKey, decryptMasterKey, KeyUnwrapError, WRAP_KEY_BYTES, WRAP_SALT_BYTES, WRAP_NONCE_BYTES, } from './key-wrap.js';
12
+ // Device secret management (US-003)
13
+ export { generateDeviceSecret, readDeviceSecret, deviceSecretExists, getDeviceKeyPath, getDeviceDir, DeviceSecretMissingError, DEVICE_SECRET_BYTES, } from './device-secret.js';
14
+ // Google OIDC provider (US-004)
15
+ export { createGoogleProvider, clearGoogleCaches, } from './providers/google.js';
16
+ // GitHub OAuth provider (US-005)
17
+ export { createGitHubProvider, } from './providers/github.js';
18
+ // Crypto helpers for vault operations (US-006)
19
+ export { deriveMasterKey, decryptWithKey, verifyMasterKey, SALT_BYTES, KEY_BYTES, NONCE_BYTES, } from './crypto-helpers.js';
20
+ // OAuth CLI commands (US-006 + US-007)
21
+ export { authLink, unlockWithOAuth, listLinkedProviders, unlinkProvider, isValidProvider, resolveProvider, MultipleProvidersError, } from './commands.js';
22
+ // OAuth config management (US-007)
23
+ export { loadOAuthConfig, saveOAuthConfig, resetOAuthConfig, updateOAuthConfig, hasCustomConfig, formatConfigForDisplay, maskSecret, getConfigPath, } from './config.js';
24
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/oauth/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,YAAY,EAAE,gBAAgB,EAAiB,MAAM,WAAW,CAAC;AAQ1E,OAAO,EACL,YAAY,EACZ,mBAAmB,EACnB,iBAAiB,EACjB,YAAY,EACZ,iBAAiB,EACjB,kBAAkB,EAClB,eAAe,GAGhB,MAAM,WAAW,CAAC;AAEnB,iDAAiD;AACjD,OAAO,EACL,aAAa,EACb,eAAe,EACf,cAAc,EACd,eAAe,EACf,mBAAmB,EACnB,aAAa,EACb,gBAAgB,EAChB,gBAAgB,EAChB,cAAc,EACd,cAAc,EACd,eAAe,EACf,gBAAgB,GAGjB,MAAM,eAAe,CAAC;AAEvB,oCAAoC;AACpC,OAAO,EACL,oBAAoB,EACpB,gBAAgB,EAChB,kBAAkB,EAClB,gBAAgB,EAChB,YAAY,EACZ,wBAAwB,EACxB,mBAAmB,GACpB,MAAM,oBAAoB,CAAC;AAE5B,gCAAgC;AAChC,OAAO,EACL,oBAAoB,EACpB,iBAAiB,GAMlB,MAAM,uBAAuB,CAAC;AAE/B,iCAAiC;AACjC,OAAO,EACL,oBAAoB,GAGrB,MAAM,uBAAuB,CAAC;AAE/B,+CAA+C;AAC/C,OAAO,EACL,eAAe,EACf,cAAc,EACd,eAAe,EACf,UAAU,EACV,SAAS,EACT,WAAW,GACZ,MAAM,qBAAqB,CAAC;AAE7B,uCAAuC;AACvC,OAAO,EACL,QAAQ,EACR,eAAe,EACf,mBAAmB,EACnB,cAAc,EACd,eAAe,EACf,eAAe,EACf,sBAAsB,GAQvB,MAAM,eAAe,CAAC;AAEvB,mCAAmC;AACnC,OAAO,EACL,eAAe,EACf,eAAe,EACf,gBAAgB,EAChB,iBAAiB,EACjB,eAAe,EACf,sBAAsB,EACtB,UAAU,EACV,aAAa,GAEd,MAAM,aAAa,CAAC"}
@@ -0,0 +1,146 @@
1
+ /**
2
+ * OAuth key wrapping — encrypt/decrypt the vault master key using an
3
+ * OAuth-derived wrap key.
4
+ *
5
+ * Flow:
6
+ * 1. User authenticates via OAuth (gets provider + subject_id)
7
+ * 2. Wrap key = HKDF-SHA256(ikm=device_secret, salt=random, info=provider+subject_id)
8
+ * 3. Wrapped key = XChaCha20-Poly1305(plaintext=master_key, key=wrap_key)
9
+ * 4. Stored in SQLite oauth_links table alongside salt and nonce
10
+ *
11
+ * Unwrap flow:
12
+ * 1. User authenticates via OAuth (gets provider + subject_id)
13
+ * 2. Look up row in oauth_links
14
+ * 3. Re-derive wrap key from device_secret + stored salt
15
+ * 4. Decrypt wrapped_key → master_key
16
+ *
17
+ * @module
18
+ */
19
+ /** Key/nonce/salt sizes */
20
+ export declare const WRAP_KEY_BYTES = 32;
21
+ export declare const WRAP_SALT_BYTES = 32;
22
+ export declare const WRAP_NONCE_BYTES = 24;
23
+ /**
24
+ * Row shape for the oauth_links SQLite table.
25
+ */
26
+ export interface OAuthLinkRow {
27
+ provider: string;
28
+ subject_id: string;
29
+ wrap_salt: Buffer;
30
+ wrapped_key: Buffer;
31
+ nonce: Buffer;
32
+ created_at: string;
33
+ last_used_at: string;
34
+ }
35
+ /**
36
+ * Minimal database interface for oauth_links table operations.
37
+ *
38
+ * This abstraction allows the key-wrap module to work with any sql.js
39
+ * database instance (typically obtained via VaultEngine.getDb().getRawDb()).
40
+ *
41
+ * sql.js Database objects expose: run(), exec(), prepare().
42
+ * Parameterized queries use prepare() → bind() → step() → getAsObject() → free().
43
+ */
44
+ export interface OAuthLinksDb {
45
+ run(sql: string, params?: unknown[]): void;
46
+ exec(sql: string): void;
47
+ prepare(sql: string): {
48
+ bind(params?: unknown[]): boolean;
49
+ step(): boolean;
50
+ getAsObject(): Record<string, unknown>;
51
+ free(): void;
52
+ };
53
+ }
54
+ /**
55
+ * Initialize the oauth_links table if it does not exist.
56
+ *
57
+ * Called once when the key-wrap module is first used against a database.
58
+ */
59
+ export declare function initOAuthLinksTable(db: OAuthLinksDb): void;
60
+ /**
61
+ * Derive a 32-byte wrap key using HKDF-SHA256.
62
+ *
63
+ * @param deviceSecret - 32-byte device secret (IKM)
64
+ * @param wrapSalt - 32-byte random salt (unique per link)
65
+ * @param provider - OAuth provider name (e.g., 'google')
66
+ * @param subjectId - Provider-specific user ID
67
+ * @returns 32-byte wrap key
68
+ */
69
+ export declare function deriveWrapKey(deviceSecret: Buffer, wrapSalt: Buffer, provider: string, subjectId: string): Buffer;
70
+ /**
71
+ * Wrap (encrypt) the master key with an OAuth-derived wrap key.
72
+ *
73
+ * Uses XChaCha20-Poly1305 for authenticated encryption.
74
+ *
75
+ * @param masterKey - The vault master key to wrap (32 bytes)
76
+ * @param wrapKey - The HKDF-derived wrap key (32 bytes)
77
+ * @returns Object containing wrapped (encrypted) key and the nonce used
78
+ */
79
+ export declare function encryptMasterKey(masterKey: Buffer, wrapKey: Buffer): Promise<{
80
+ wrappedKey: Buffer;
81
+ nonce: Buffer;
82
+ }>;
83
+ /**
84
+ * Unwrap (decrypt) the master key with an OAuth-derived wrap key.
85
+ *
86
+ * Uses XChaCha20-Poly1305 for authenticated decryption.
87
+ *
88
+ * @param wrappedKey - The encrypted master key
89
+ * @param nonce - The nonce used during encryption
90
+ * @param wrapKey - The HKDF-derived wrap key (32 bytes)
91
+ * @returns The decrypted master key (32 bytes)
92
+ * @throws Error if the wrap key is wrong or data is corrupted
93
+ */
94
+ export declare function decryptMasterKey(wrappedKey: Buffer, nonce: Buffer, wrapKey: Buffer): Promise<Buffer>;
95
+ /**
96
+ * Error thrown when key unwrapping fails.
97
+ */
98
+ export declare class KeyUnwrapError extends Error {
99
+ constructor(message: string);
100
+ }
101
+ /**
102
+ * Wrap the master key for an OAuth identity and store in the database.
103
+ *
104
+ * This is the main entry point for linking an OAuth identity.
105
+ * Called after the user authenticates via OAuth while the vault is unlocked.
106
+ *
107
+ * @param db - sql.js database instance (from VaultEngine.getDb().getRawDb())
108
+ * @param masterKey - The current vault master key (32 bytes)
109
+ * @param provider - OAuth provider name (e.g., 'google', 'github')
110
+ * @param subjectId - Provider-specific unique user ID
111
+ * @param deviceDir - Optional directory for device.key (default: ~/.yokotoken)
112
+ */
113
+ export declare function wrapMasterKey(db: OAuthLinksDb, masterKey: Buffer, provider: string, subjectId: string, deviceDir?: string): Promise<void>;
114
+ /**
115
+ * Unwrap the master key using an OAuth identity.
116
+ *
117
+ * This is the main entry point for OAuth-based vault unlock.
118
+ * Called after the user authenticates via OAuth.
119
+ *
120
+ * @param db - sql.js database instance (from VaultEngine.getDb().getRawDb())
121
+ * @param provider - OAuth provider name (e.g., 'google', 'github')
122
+ * @param subjectId - Provider-specific unique user ID
123
+ * @param deviceDir - Optional directory for device.key (default: ~/.yokotoken)
124
+ * @returns The decrypted vault master key (32 bytes)
125
+ * @throws KeyUnwrapError if decryption fails (wrong identity)
126
+ * @throws DeviceSecretMissingError if device.key is not found
127
+ * @throws Error if no link exists for the given provider+subject_id
128
+ */
129
+ export declare function unwrapMasterKey(db: OAuthLinksDb, provider: string, subjectId: string, deviceDir?: string): Promise<Buffer>;
130
+ /**
131
+ * List all linked OAuth providers for the current vault.
132
+ *
133
+ * @param db - sql.js database instance
134
+ * @returns Array of link rows (provider, subject_id, created_at, last_used_at)
135
+ */
136
+ export declare function listOAuthLinks(db: OAuthLinksDb): OAuthLinkRow[];
137
+ /**
138
+ * Remove an OAuth link for a provider.
139
+ *
140
+ * @param db - sql.js database instance
141
+ * @param provider - OAuth provider name
142
+ * @param subjectId - Optional subject ID (if omitted, removes all links for the provider)
143
+ * @returns true if a row was deleted
144
+ */
145
+ export declare function removeOAuthLink(db: OAuthLinksDb, provider: string, subjectId?: string): boolean;
146
+ //# sourceMappingURL=key-wrap.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"key-wrap.d.ts","sourceRoot":"","sources":["../../src/oauth/key-wrap.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AASH,2BAA2B;AAC3B,eAAO,MAAM,cAAc,KAAK,CAAC;AACjC,eAAO,MAAM,eAAe,KAAK,CAAC;AAClC,eAAO,MAAM,gBAAgB,KAAK,CAAC;AAWnC;;GAEG;AACH,MAAM,WAAW,YAAY;IAC3B,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,EAAE,MAAM,CAAC;IACnB,YAAY,EAAE,MAAM,CAAC;CACtB;AAED;;;;;;;;GAQG;AACH,MAAM,WAAW,YAAY;IAC3B,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,OAAO,EAAE,GAAG,IAAI,CAAC;IAC3C,IAAI,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,OAAO,CAAC,GAAG,EAAE,MAAM,GAAG;QACpB,IAAI,CAAC,MAAM,CAAC,EAAE,OAAO,EAAE,GAAG,OAAO,CAAC;QAClC,IAAI,IAAI,OAAO,CAAC;QAChB,WAAW,IAAI,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;QACvC,IAAI,IAAI,IAAI,CAAC;KACd,CAAC;CACH;AAiBD;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,EAAE,EAAE,YAAY,GAAG,IAAI,CAa1D;AAED;;;;;;;;GAQG;AACH,wBAAgB,aAAa,CAC3B,YAAY,EAAE,MAAM,EACpB,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,MAAM,EAChB,SAAS,EAAE,MAAM,GAChB,MAAM,CAaR;AAED;;;;;;;;GAQG;AACH,wBAAsB,gBAAgB,CACpC,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,MAAM,GACd,OAAO,CAAC;IAAE,UAAU,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC,CAiBhD;AAED;;;;;;;;;;GAUG;AACH,wBAAsB,gBAAgB,CACpC,UAAU,EAAE,MAAM,EAClB,KAAK,EAAE,MAAM,EACb,OAAO,EAAE,MAAM,GACd,OAAO,CAAC,MAAM,CAAC,CAqBjB;AAED;;GAEG;AACH,qBAAa,cAAe,SAAQ,KAAK;gBAC3B,OAAO,EAAE,MAAM;CAI5B;AAID;;;;;;;;;;;GAWG;AACH,wBAAsB,aAAa,CACjC,EAAE,EAAE,YAAY,EAChB,SAAS,EAAE,MAAM,EACjB,QAAQ,EAAE,MAAM,EAChB,SAAS,EAAE,MAAM,EACjB,SAAS,CAAC,EAAE,MAAM,GACjB,OAAO,CAAC,IAAI,CAAC,CAiCf;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAsB,eAAe,CACnC,EAAE,EAAE,YAAY,EAChB,QAAQ,EAAE,MAAM,EAChB,SAAS,EAAE,MAAM,EACjB,SAAS,CAAC,EAAE,MAAM,GACjB,OAAO,CAAC,MAAM,CAAC,CAgDjB;AAED;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,EAAE,EAAE,YAAY,GAAG,YAAY,EAAE,CAwB/D;AAED;;;;;;;GAOG;AACH,wBAAgB,eAAe,CAC7B,EAAE,EAAE,YAAY,EAChB,QAAQ,EAAE,MAAM,EAChB,SAAS,CAAC,EAAE,MAAM,GACjB,OAAO,CAyBT"}
@@ -0,0 +1,275 @@
1
+ /**
2
+ * OAuth key wrapping — encrypt/decrypt the vault master key using an
3
+ * OAuth-derived wrap key.
4
+ *
5
+ * Flow:
6
+ * 1. User authenticates via OAuth (gets provider + subject_id)
7
+ * 2. Wrap key = HKDF-SHA256(ikm=device_secret, salt=random, info=provider+subject_id)
8
+ * 3. Wrapped key = XChaCha20-Poly1305(plaintext=master_key, key=wrap_key)
9
+ * 4. Stored in SQLite oauth_links table alongside salt and nonce
10
+ *
11
+ * Unwrap flow:
12
+ * 1. User authenticates via OAuth (gets provider + subject_id)
13
+ * 2. Look up row in oauth_links
14
+ * 3. Re-derive wrap key from device_secret + stored salt
15
+ * 4. Decrypt wrapped_key → master_key
16
+ *
17
+ * @module
18
+ */
19
+ import { hkdfSync, randomBytes } from 'node:crypto';
20
+ import { readDeviceSecret, generateDeviceSecret } from './device-secret.js';
21
+ // Libsodium is loaded via yokotoken's transitive dependency.
22
+ // We import it the same way yokotoken's crypto module does.
23
+ import sodium from 'libsodium-wrappers-sumo';
24
+ /** Key/nonce/salt sizes */
25
+ export const WRAP_KEY_BYTES = 32;
26
+ export const WRAP_SALT_BYTES = 32;
27
+ export const WRAP_NONCE_BYTES = 24; // XChaCha20-Poly1305
28
+ /** Ensure libsodium WASM is initialized. */
29
+ let sodiumReady = false;
30
+ async function ensureSodium() {
31
+ if (!sodiumReady) {
32
+ await sodium.ready;
33
+ sodiumReady = true;
34
+ }
35
+ }
36
+ /**
37
+ * Execute a parameterized query and return a single row as an object.
38
+ * Returns an empty object if no rows match.
39
+ */
40
+ function queryOne(db, sql, params) {
41
+ const stmt = db.prepare(sql);
42
+ stmt.bind(params);
43
+ let result = {};
44
+ if (stmt.step()) {
45
+ result = stmt.getAsObject();
46
+ }
47
+ stmt.free();
48
+ return result;
49
+ }
50
+ /**
51
+ * Initialize the oauth_links table if it does not exist.
52
+ *
53
+ * Called once when the key-wrap module is first used against a database.
54
+ */
55
+ export function initOAuthLinksTable(db) {
56
+ db.exec(`
57
+ CREATE TABLE IF NOT EXISTS oauth_links (
58
+ provider TEXT NOT NULL,
59
+ subject_id TEXT NOT NULL,
60
+ wrap_salt BLOB NOT NULL,
61
+ wrapped_key BLOB NOT NULL,
62
+ nonce BLOB NOT NULL,
63
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
64
+ last_used_at TEXT NOT NULL DEFAULT (datetime('now')),
65
+ PRIMARY KEY (provider, subject_id)
66
+ )
67
+ `);
68
+ }
69
+ /**
70
+ * Derive a 32-byte wrap key using HKDF-SHA256.
71
+ *
72
+ * @param deviceSecret - 32-byte device secret (IKM)
73
+ * @param wrapSalt - 32-byte random salt (unique per link)
74
+ * @param provider - OAuth provider name (e.g., 'google')
75
+ * @param subjectId - Provider-specific user ID
76
+ * @returns 32-byte wrap key
77
+ */
78
+ export function deriveWrapKey(deviceSecret, wrapSalt, provider, subjectId) {
79
+ // info = "unotoken-oauth:" + provider + ":" + subjectId
80
+ const info = `unotoken-oauth:${provider}:${subjectId}`;
81
+ const key = hkdfSync('sha256', deviceSecret, // IKM (input keying material)
82
+ wrapSalt, // salt
83
+ info, // info/context string
84
+ WRAP_KEY_BYTES);
85
+ return Buffer.from(key);
86
+ }
87
+ /**
88
+ * Wrap (encrypt) the master key with an OAuth-derived wrap key.
89
+ *
90
+ * Uses XChaCha20-Poly1305 for authenticated encryption.
91
+ *
92
+ * @param masterKey - The vault master key to wrap (32 bytes)
93
+ * @param wrapKey - The HKDF-derived wrap key (32 bytes)
94
+ * @returns Object containing wrapped (encrypted) key and the nonce used
95
+ */
96
+ export async function encryptMasterKey(masterKey, wrapKey) {
97
+ await ensureSodium();
98
+ const nonce = sodium.randombytes_buf(WRAP_NONCE_BYTES);
99
+ const ciphertext = sodium.crypto_aead_xchacha20poly1305_ietf_encrypt(new Uint8Array(masterKey), null, // no additional data
100
+ null, // nsec (unused, always null)
101
+ nonce, new Uint8Array(wrapKey));
102
+ return {
103
+ wrappedKey: Buffer.from(ciphertext),
104
+ nonce: Buffer.from(nonce),
105
+ };
106
+ }
107
+ /**
108
+ * Unwrap (decrypt) the master key with an OAuth-derived wrap key.
109
+ *
110
+ * Uses XChaCha20-Poly1305 for authenticated decryption.
111
+ *
112
+ * @param wrappedKey - The encrypted master key
113
+ * @param nonce - The nonce used during encryption
114
+ * @param wrapKey - The HKDF-derived wrap key (32 bytes)
115
+ * @returns The decrypted master key (32 bytes)
116
+ * @throws Error if the wrap key is wrong or data is corrupted
117
+ */
118
+ export async function decryptMasterKey(wrappedKey, nonce, wrapKey) {
119
+ await ensureSodium();
120
+ try {
121
+ const plaintext = sodium.crypto_aead_xchacha20poly1305_ietf_decrypt(null, // nsec (unused)
122
+ new Uint8Array(wrappedKey), null, // no additional data
123
+ new Uint8Array(nonce), new Uint8Array(wrapKey));
124
+ return Buffer.from(plaintext);
125
+ }
126
+ catch {
127
+ throw new KeyUnwrapError('Failed to unwrap master key: wrong identity or corrupted data. ' +
128
+ 'The OAuth account used may not match the one that was linked, ' +
129
+ 'or the device.key file may have changed. ' +
130
+ 'Use your passphrase to unlock and re-link with \'unotoken auth link <provider>\'.');
131
+ }
132
+ }
133
+ /**
134
+ * Error thrown when key unwrapping fails.
135
+ */
136
+ export class KeyUnwrapError extends Error {
137
+ constructor(message) {
138
+ super(message);
139
+ this.name = 'KeyUnwrapError';
140
+ }
141
+ }
142
+ // ─── High-level wrap/unwrap operations ───────────────────────────────
143
+ /**
144
+ * Wrap the master key for an OAuth identity and store in the database.
145
+ *
146
+ * This is the main entry point for linking an OAuth identity.
147
+ * Called after the user authenticates via OAuth while the vault is unlocked.
148
+ *
149
+ * @param db - sql.js database instance (from VaultEngine.getDb().getRawDb())
150
+ * @param masterKey - The current vault master key (32 bytes)
151
+ * @param provider - OAuth provider name (e.g., 'google', 'github')
152
+ * @param subjectId - Provider-specific unique user ID
153
+ * @param deviceDir - Optional directory for device.key (default: ~/.yokotoken)
154
+ */
155
+ export async function wrapMasterKey(db, masterKey, provider, subjectId, deviceDir) {
156
+ // Ensure table exists
157
+ initOAuthLinksTable(db);
158
+ // Generate or read device secret
159
+ const deviceSecret = generateDeviceSecret(deviceDir);
160
+ // Generate random salt for this link
161
+ const wrapSalt = randomBytes(WRAP_SALT_BYTES);
162
+ // Derive wrap key via HKDF
163
+ const wrapKey = deriveWrapKey(deviceSecret, wrapSalt, provider, subjectId);
164
+ // Encrypt master key
165
+ const { wrappedKey, nonce } = await encryptMasterKey(masterKey, wrapKey);
166
+ // Securely zero the wrap key
167
+ try {
168
+ await ensureSodium();
169
+ sodium.memzero(new Uint8Array(wrapKey.buffer, wrapKey.byteOffset, wrapKey.byteLength));
170
+ }
171
+ catch {
172
+ // Best-effort secure zeroing
173
+ }
174
+ const now = new Date().toISOString();
175
+ // Upsert into oauth_links (replace if provider+subject_id already linked)
176
+ db.run(`INSERT OR REPLACE INTO oauth_links
177
+ (provider, subject_id, wrap_salt, wrapped_key, nonce, created_at, last_used_at)
178
+ VALUES (?, ?, ?, ?, ?, ?, ?)`, [provider, subjectId, wrapSalt, wrappedKey, nonce, now, now]);
179
+ }
180
+ /**
181
+ * Unwrap the master key using an OAuth identity.
182
+ *
183
+ * This is the main entry point for OAuth-based vault unlock.
184
+ * Called after the user authenticates via OAuth.
185
+ *
186
+ * @param db - sql.js database instance (from VaultEngine.getDb().getRawDb())
187
+ * @param provider - OAuth provider name (e.g., 'google', 'github')
188
+ * @param subjectId - Provider-specific unique user ID
189
+ * @param deviceDir - Optional directory for device.key (default: ~/.yokotoken)
190
+ * @returns The decrypted vault master key (32 bytes)
191
+ * @throws KeyUnwrapError if decryption fails (wrong identity)
192
+ * @throws DeviceSecretMissingError if device.key is not found
193
+ * @throws Error if no link exists for the given provider+subject_id
194
+ */
195
+ export async function unwrapMasterKey(db, provider, subjectId, deviceDir) {
196
+ // Ensure table exists
197
+ initOAuthLinksTable(db);
198
+ // Read device secret (throws DeviceSecretMissingError if missing)
199
+ const deviceSecret = readDeviceSecret(deviceDir);
200
+ // Look up the link
201
+ const row = queryOne(db, `SELECT wrap_salt, wrapped_key, nonce FROM oauth_links
202
+ WHERE provider = ? AND subject_id = ?`, [provider, subjectId]);
203
+ if (!row || !row.wrap_salt) {
204
+ throw new Error(`No OAuth link found for provider '${provider}' with subject '${subjectId}'. ` +
205
+ `Run 'unotoken auth link ${provider}' while the vault is unlocked to create a link.`);
206
+ }
207
+ // Extract stored values
208
+ const wrapSalt = Buffer.from(row.wrap_salt);
209
+ const wrappedKey = Buffer.from(row.wrapped_key);
210
+ const nonce = Buffer.from(row.nonce);
211
+ // Re-derive wrap key from device secret + stored salt
212
+ const wrapKey = deriveWrapKey(deviceSecret, wrapSalt, provider, subjectId);
213
+ // Decrypt master key
214
+ const masterKey = await decryptMasterKey(wrappedKey, nonce, wrapKey);
215
+ // Update last_used_at
216
+ db.run(`UPDATE oauth_links SET last_used_at = ? WHERE provider = ? AND subject_id = ?`, [new Date().toISOString(), provider, subjectId]);
217
+ // Securely zero the wrap key
218
+ try {
219
+ await ensureSodium();
220
+ sodium.memzero(new Uint8Array(wrapKey.buffer, wrapKey.byteOffset, wrapKey.byteLength));
221
+ }
222
+ catch {
223
+ // Best-effort secure zeroing
224
+ }
225
+ return masterKey;
226
+ }
227
+ /**
228
+ * List all linked OAuth providers for the current vault.
229
+ *
230
+ * @param db - sql.js database instance
231
+ * @returns Array of link rows (provider, subject_id, created_at, last_used_at)
232
+ */
233
+ export function listOAuthLinks(db) {
234
+ initOAuthLinksTable(db);
235
+ const links = [];
236
+ const stmt = db.prepare(`SELECT provider, subject_id, wrap_salt, wrapped_key, nonce, created_at, last_used_at
237
+ FROM oauth_links ORDER BY created_at ASC`);
238
+ while (stmt.step()) {
239
+ const row = stmt.getAsObject();
240
+ links.push({
241
+ provider: row.provider,
242
+ subject_id: row.subject_id,
243
+ wrap_salt: Buffer.from(row.wrap_salt),
244
+ wrapped_key: Buffer.from(row.wrapped_key),
245
+ nonce: Buffer.from(row.nonce),
246
+ created_at: row.created_at,
247
+ last_used_at: row.last_used_at,
248
+ });
249
+ }
250
+ stmt.free();
251
+ return links;
252
+ }
253
+ /**
254
+ * Remove an OAuth link for a provider.
255
+ *
256
+ * @param db - sql.js database instance
257
+ * @param provider - OAuth provider name
258
+ * @param subjectId - Optional subject ID (if omitted, removes all links for the provider)
259
+ * @returns true if a row was deleted
260
+ */
261
+ export function removeOAuthLink(db, provider, subjectId) {
262
+ initOAuthLinksTable(db);
263
+ // Count before delete
264
+ const before = queryOne(db, subjectId
265
+ ? `SELECT COUNT(*) as cnt FROM oauth_links WHERE provider = ? AND subject_id = ?`
266
+ : `SELECT COUNT(*) as cnt FROM oauth_links WHERE provider = ?`, subjectId ? [provider, subjectId] : [provider]);
267
+ if (subjectId) {
268
+ db.run(`DELETE FROM oauth_links WHERE provider = ? AND subject_id = ?`, [provider, subjectId]);
269
+ }
270
+ else {
271
+ db.run(`DELETE FROM oauth_links WHERE provider = ?`, [provider]);
272
+ }
273
+ return before.cnt > 0;
274
+ }
275
+ //# sourceMappingURL=key-wrap.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"key-wrap.js","sourceRoot":"","sources":["../../src/oauth/key-wrap.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,29 @@
1
+ /**
2
+ * PKCE (Proof Key for Code Exchange) utilities for OAuth 2.0.
3
+ *
4
+ * Uses Node's built-in crypto module — no external dependencies.
5
+ * Implements S256 method (SHA-256 of verifier as challenge).
6
+ *
7
+ * @see https://datatracker.ietf.org/doc/html/rfc7636
8
+ */
9
+ export interface PKCEPair {
10
+ /** Random high-entropy string (43-128 chars, unreserved URI characters) */
11
+ verifier: string;
12
+ /** Base64url-encoded SHA-256 hash of the verifier */
13
+ challenge: string;
14
+ /** Always 'S256' */
15
+ method: 'S256';
16
+ }
17
+ /**
18
+ * Generate a PKCE verifier and challenge pair.
19
+ *
20
+ * The verifier is 64 bytes of random data, base64url-encoded (86 chars).
21
+ * The challenge is the SHA-256 hash of the verifier, base64url-encoded.
22
+ */
23
+ export declare function generatePKCE(): PKCEPair;
24
+ /**
25
+ * Compute the S256 challenge from a verifier.
26
+ * Useful for verification in tests.
27
+ */
28
+ export declare function computeChallenge(verifier: string): string;
29
+ //# sourceMappingURL=pkce.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pkce.d.ts","sourceRoot":"","sources":["../../src/oauth/pkce.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAIH,MAAM,WAAW,QAAQ;IACvB,2EAA2E;IAC3E,QAAQ,EAAE,MAAM,CAAC;IACjB,qDAAqD;IACrD,SAAS,EAAE,MAAM,CAAC;IAClB,oBAAoB;IACpB,MAAM,EAAE,MAAM,CAAC;CAChB;AAED;;;;;GAKG;AACH,wBAAgB,YAAY,IAAI,QAAQ,CAIvC;AAED;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAEzD"}
@@ -0,0 +1,34 @@
1
+ /**
2
+ * PKCE (Proof Key for Code Exchange) utilities for OAuth 2.0.
3
+ *
4
+ * Uses Node's built-in crypto module — no external dependencies.
5
+ * Implements S256 method (SHA-256 of verifier as challenge).
6
+ *
7
+ * @see https://datatracker.ietf.org/doc/html/rfc7636
8
+ */
9
+ import { randomBytes, createHash } from 'node:crypto';
10
+ /**
11
+ * Generate a PKCE verifier and challenge pair.
12
+ *
13
+ * The verifier is 64 bytes of random data, base64url-encoded (86 chars).
14
+ * The challenge is the SHA-256 hash of the verifier, base64url-encoded.
15
+ */
16
+ export function generatePKCE() {
17
+ const verifier = base64url(randomBytes(64));
18
+ const challenge = base64url(createHash('sha256').update(verifier).digest());
19
+ return { verifier, challenge, method: 'S256' };
20
+ }
21
+ /**
22
+ * Compute the S256 challenge from a verifier.
23
+ * Useful for verification in tests.
24
+ */
25
+ export function computeChallenge(verifier) {
26
+ return base64url(createHash('sha256').update(verifier).digest());
27
+ }
28
+ /**
29
+ * Base64url encoding (RFC 4648 section 5) without padding.
30
+ */
31
+ function base64url(buf) {
32
+ return buf.toString('base64url');
33
+ }
34
+ //# sourceMappingURL=pkce.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pkce.js","sourceRoot":"","sources":["../../src/oauth/pkce.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAWtD;;;;;GAKG;AACH,MAAM,UAAU,YAAY;IAC1B,MAAM,QAAQ,GAAG,SAAS,CAAC,WAAW,CAAC,EAAE,CAAC,CAAC,CAAC;IAC5C,MAAM,SAAS,GAAG,SAAS,CAAC,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC;IAC5E,OAAO,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,CAAC;AACjD,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,gBAAgB,CAAC,QAAgB;IAC/C,OAAO,SAAS,CAAC,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC;AACnE,CAAC;AAED;;GAEG;AACH,SAAS,SAAS,CAAC,GAAW;IAC5B,OAAO,GAAG,CAAC,QAAQ,CAAC,WAAW,CAAC,CAAC;AACnC,CAAC"}
@@ -0,0 +1,79 @@
1
+ /**
2
+ * OAuth provider interface for unotoken.
3
+ *
4
+ * Each provider (Google, GitHub, etc.) implements this interface to
5
+ * plug into the generic OAuthFlow engine. The flow engine handles
6
+ * PKCE, callback server, and browser opening — providers just define
7
+ * their endpoints and token verification logic.
8
+ */
9
+ import type { PKCEPair } from './pkce.js';
10
+ /**
11
+ * Result of a successful OAuth authentication.
12
+ */
13
+ export interface OAuthIdentity {
14
+ /** Provider name (e.g., 'google', 'github') */
15
+ provider: string;
16
+ /** Provider-specific unique user ID (e.g., Google sub, GitHub user ID) */
17
+ subjectId: string;
18
+ /** User's email address (if available) */
19
+ email?: string;
20
+ /** User's display name (if available) */
21
+ name?: string;
22
+ }
23
+ /**
24
+ * Token response from the OAuth token endpoint.
25
+ */
26
+ export interface TokenResponse {
27
+ access_token: string;
28
+ token_type: string;
29
+ expires_in?: number;
30
+ refresh_token?: string;
31
+ id_token?: string;
32
+ scope?: string;
33
+ }
34
+ /**
35
+ * Provider interface — each OAuth provider must implement this.
36
+ */
37
+ export interface OAuthProvider {
38
+ /** Provider identifier (e.g., 'google', 'github') */
39
+ readonly name: string;
40
+ /** OAuth client ID */
41
+ readonly clientId: string;
42
+ /** Requested scopes */
43
+ readonly scopes: string[];
44
+ /**
45
+ * Build the authorization URL for the browser redirect.
46
+ *
47
+ * @param redirectUri - The localhost callback URL (e.g., http://localhost:12345/callback)
48
+ * @param state - CSRF protection state parameter
49
+ * @param pkce - PKCE verifier/challenge pair
50
+ * @returns Full authorization URL to open in the browser
51
+ */
52
+ authorizeUrl(redirectUri: string, state: string, pkce: PKCEPair): string;
53
+ /** Token endpoint URL */
54
+ readonly tokenEndpoint: string;
55
+ /**
56
+ * Build the body parameters for the token exchange request.
57
+ *
58
+ * Most providers use the standard OAuth2 code exchange, but some (like GitHub)
59
+ * require additional parameters (e.g., client_secret).
60
+ *
61
+ * @param code - Authorization code from the callback
62
+ * @param redirectUri - The same redirect URI used in the authorize request
63
+ * @param pkce - PKCE pair (verifier is sent to prove possession)
64
+ * @returns URLSearchParams for the POST body
65
+ */
66
+ tokenRequestBody(code: string, redirectUri: string, pkce: PKCEPair): URLSearchParams;
67
+ /**
68
+ * Verify and extract identity from the token response.
69
+ *
70
+ * For OIDC providers (Google): verify the ID token signature, expiry, audience, issuer.
71
+ * For non-OIDC providers (GitHub): use the access token to call a user info endpoint.
72
+ *
73
+ * @param tokenResponse - Raw token response from the token endpoint
74
+ * @returns Verified identity
75
+ * @throws Error if verification fails
76
+ */
77
+ verifyIdentity(tokenResponse: TokenResponse): Promise<OAuthIdentity>;
78
+ }
79
+ //# sourceMappingURL=provider.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"provider.d.ts","sourceRoot":"","sources":["../../src/oauth/provider.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAC;AAE1C;;GAEG;AACH,MAAM,WAAW,aAAa;IAC5B,+CAA+C;IAC/C,QAAQ,EAAE,MAAM,CAAC;IACjB,0EAA0E;IAC1E,SAAS,EAAE,MAAM,CAAC;IAClB,0CAA0C;IAC1C,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,yCAAyC;IACzC,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED;;GAEG;AACH,MAAM,WAAW,aAAa;IAC5B,YAAY,EAAE,MAAM,CAAC;IACrB,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED;;GAEG;AACH,MAAM,WAAW,aAAa;IAC5B,qDAAqD;IACrD,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IAEtB,sBAAsB;IACtB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAE1B,uBAAuB;IACvB,QAAQ,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC;IAE1B;;;;;;;OAOG;IACH,YAAY,CAAC,WAAW,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,QAAQ,GAAG,MAAM,CAAC;IAEzE,yBAAyB;IACzB,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAE/B;;;;;;;;;;OAUG;IACH,gBAAgB,CAAC,IAAI,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,EAAE,IAAI,EAAE,QAAQ,GAAG,eAAe,CAAC;IAErF;;;;;;;;;OASG;IACH,cAAc,CAAC,aAAa,EAAE,aAAa,GAAG,OAAO,CAAC,aAAa,CAAC,CAAC;CACtE"}
@@ -0,0 +1,10 @@
1
+ /**
2
+ * OAuth provider interface for unotoken.
3
+ *
4
+ * Each provider (Google, GitHub, etc.) implements this interface to
5
+ * plug into the generic OAuthFlow engine. The flow engine handles
6
+ * PKCE, callback server, and browser opening — providers just define
7
+ * their endpoints and token verification logic.
8
+ */
9
+ export {};
10
+ //# sourceMappingURL=provider.js.map