@opendatalabs/vana-sdk 3.0.1 → 3.2.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.
- package/README.md +2 -1
- package/dist/__tests__/interop-personal-server.test.d.ts +1 -0
- package/dist/account/personal-server-lite-owner-binding.cjs +81 -0
- package/dist/account/personal-server-lite-owner-binding.cjs.map +1 -0
- package/dist/account/personal-server-lite-owner-binding.d.ts +30 -0
- package/dist/account/personal-server-lite-owner-binding.js +59 -0
- package/dist/account/personal-server-lite-owner-binding.js.map +1 -0
- package/dist/account/personal-server-lite-owner-binding.test.d.ts +1 -0
- package/dist/account/personal-server-registration.cjs +263 -0
- package/dist/account/personal-server-registration.cjs.map +1 -0
- package/dist/account/personal-server-registration.d.ts +66 -0
- package/dist/account/personal-server-registration.js +240 -0
- package/dist/account/personal-server-registration.js.map +1 -0
- package/dist/account/personal-server-registration.test.d.ts +1 -0
- package/dist/auth/errors.js +1 -1
- package/dist/auth/oauth-client.cjs +250 -0
- package/dist/auth/oauth-client.cjs.map +1 -0
- package/dist/auth/oauth-client.d.ts +90 -0
- package/dist/auth/oauth-client.js +228 -0
- package/dist/auth/oauth-client.js.map +1 -0
- package/dist/auth/oauth-client.test.d.ts +1 -0
- package/dist/auth/web3-signed-builder.js +1 -1
- package/dist/auth/web3-signed.js +3 -3
- package/dist/browser.js +1 -1
- package/dist/chains/definitions.js +1 -1
- package/dist/chains/index.js +1 -1
- package/dist/chains.browser.js +1 -1
- package/dist/chains.js +1 -1
- package/dist/chains.node.js +1 -1
- package/dist/contracts/contractController.js +4 -4
- package/dist/core/client.js +1 -1
- package/dist/crypto/ecies/base.js +3 -3
- package/dist/crypto/ecies/browser.js +1 -1
- package/dist/crypto/ecies/index.js +1 -1
- package/dist/crypto/ecies/interface.js +1 -1
- package/dist/crypto/ecies/node.js +1 -1
- package/dist/crypto/services/WalletKeyEncryptionService.js +1 -1
- package/dist/generated/abi/index.js +26 -26
- package/dist/index.browser.d.ts +5 -0
- package/dist/index.browser.js +671 -4
- package/dist/index.browser.js.map +4 -4
- package/dist/index.node.cjs +689 -6
- package/dist/index.node.cjs.map +4 -4
- package/dist/index.node.d.ts +5 -0
- package/dist/index.node.js +671 -4
- package/dist/index.node.js.map +4 -4
- package/dist/node.js +1 -1
- package/dist/platform/browser-only.js +1 -1
- package/dist/platform/browser-safe.js +1 -1
- package/dist/platform/browser.js +6 -6
- package/dist/platform/index.js +4 -4
- package/dist/platform/node.js +8 -8
- package/dist/platform/utils.js +2 -2
- package/dist/platform.browser.js +3 -3
- package/dist/platform.js +4 -4
- package/dist/platform.node.js +4 -4
- package/dist/protocol/eip712.cjs.map +1 -1
- package/dist/protocol/eip712.d.ts +1 -1
- package/dist/protocol/eip712.js.map +1 -1
- package/dist/protocol/grants.js +1 -1
- package/dist/protocol/personal-server-lite-owner-binding.cjs +93 -0
- package/dist/protocol/personal-server-lite-owner-binding.cjs.map +1 -0
- package/dist/protocol/personal-server-lite-owner-binding.d.ts +44 -0
- package/dist/protocol/personal-server-lite-owner-binding.js +65 -0
- package/dist/protocol/personal-server-lite-owner-binding.js.map +1 -0
- package/dist/protocol/personal-server-lite-owner-binding.test.d.ts +1 -0
- package/dist/protocol/personal-server-registration.cjs +122 -0
- package/dist/protocol/personal-server-registration.cjs.map +1 -0
- package/dist/protocol/personal-server-registration.d.ts +62 -0
- package/dist/protocol/personal-server-registration.js +97 -0
- package/dist/protocol/personal-server-registration.js.map +1 -0
- package/dist/protocol/personal-server-registration.test.d.ts +1 -0
- package/dist/storage/default.js +1 -1
- package/dist/storage/index.cjs.map +1 -1
- package/dist/storage/index.d.ts +1 -1
- package/dist/storage/index.js +10 -10
- package/dist/storage/index.js.map +1 -1
- package/dist/storage/manager.js +1 -1
- package/dist/storage/providers/callback-storage.js +1 -1
- package/dist/storage/providers/dropbox.js +1 -1
- package/dist/storage/providers/google-drive.js +1 -1
- package/dist/storage/providers/ipfs.js +2 -2
- package/dist/storage/providers/pinata.js +1 -1
- package/dist/storage/providers/r2.js +1 -1
- package/dist/storage/providers/vana-storage.cjs +1 -1
- package/dist/storage/providers/vana-storage.cjs.map +1 -1
- package/dist/storage/providers/vana-storage.d.ts +2 -2
- package/dist/storage/providers/vana-storage.js +3 -3
- package/dist/storage/providers/vana-storage.js.map +1 -1
- package/dist/types/index.js +2 -2
- package/dist/types.js +1 -1
- package/package.json +5 -2
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
import { isAddress } from "viem";
|
|
2
|
+
import {
|
|
3
|
+
buildPersonalServerRegistrationTypedData,
|
|
4
|
+
personalServerRegistrationDomain
|
|
5
|
+
} from "../protocol/personal-server-registration.js";
|
|
6
|
+
import { SERVER_REGISTRATION_TYPES } from "../protocol/eip712.js";
|
|
7
|
+
const ACCOUNT_PERSONAL_SERVER_REGISTRATION_INTENT = "personal_server.server_registration.v1";
|
|
8
|
+
class AccountPersonalServerRegistrationError extends Error {
|
|
9
|
+
status;
|
|
10
|
+
code;
|
|
11
|
+
details;
|
|
12
|
+
constructor(input) {
|
|
13
|
+
super(input.message);
|
|
14
|
+
this.name = "AccountPersonalServerRegistrationError";
|
|
15
|
+
this.status = input.status;
|
|
16
|
+
this.code = input.code;
|
|
17
|
+
this.details = input.details;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
const DEFAULT_ACCOUNT_PS_REGISTRATION_PATH = "/api/v1/intents/personal-server-registration/sign";
|
|
21
|
+
function trimTrailingSlash(value) {
|
|
22
|
+
return value.replace(/\/+$/, "");
|
|
23
|
+
}
|
|
24
|
+
function assertAddress(value, name) {
|
|
25
|
+
if (!isAddress(value)) {
|
|
26
|
+
throw new Error(`${name} must be a valid EVM address`);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
async function parseAccountResponse(response) {
|
|
30
|
+
const body = await response.json().catch(() => void 0);
|
|
31
|
+
if (!response.ok) {
|
|
32
|
+
throw new AccountPersonalServerRegistrationError({
|
|
33
|
+
status: response.status,
|
|
34
|
+
code: accountErrorCode(body),
|
|
35
|
+
message: accountErrorMessage(response.status, body),
|
|
36
|
+
details: body
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
return body;
|
|
40
|
+
}
|
|
41
|
+
function accountErrorMessage(status, body) {
|
|
42
|
+
const nestedMessage = nestedAccountErrorField(body, "message");
|
|
43
|
+
if (nestedMessage) {
|
|
44
|
+
return nestedMessage;
|
|
45
|
+
}
|
|
46
|
+
if (isRecord(body) && typeof body.message === "string") {
|
|
47
|
+
return body.message;
|
|
48
|
+
}
|
|
49
|
+
const code = accountErrorCode(body);
|
|
50
|
+
if (code) {
|
|
51
|
+
return `Account PS registration signing failed: ${code}`;
|
|
52
|
+
}
|
|
53
|
+
return `Account PS registration signing failed: ${status}`;
|
|
54
|
+
}
|
|
55
|
+
function accountErrorCode(body) {
|
|
56
|
+
const nestedCode = nestedAccountErrorField(body, "code");
|
|
57
|
+
if (nestedCode) {
|
|
58
|
+
return nestedCode;
|
|
59
|
+
}
|
|
60
|
+
if (isRecord(body)) {
|
|
61
|
+
if (typeof body.code === "string") {
|
|
62
|
+
return body.code;
|
|
63
|
+
}
|
|
64
|
+
if (typeof body.error === "string") {
|
|
65
|
+
return body.error;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return void 0;
|
|
69
|
+
}
|
|
70
|
+
function nestedAccountErrorField(body, field) {
|
|
71
|
+
if (!isRecord(body) || !isRecord(body.error)) {
|
|
72
|
+
return void 0;
|
|
73
|
+
}
|
|
74
|
+
const value = body.error[field];
|
|
75
|
+
return typeof value === "string" ? value : void 0;
|
|
76
|
+
}
|
|
77
|
+
function isRecord(value) {
|
|
78
|
+
return typeof value === "object" && value !== null;
|
|
79
|
+
}
|
|
80
|
+
function normalizeAccountResponse(response) {
|
|
81
|
+
return {
|
|
82
|
+
...response,
|
|
83
|
+
status: response.status === "fallback_required" ? "confirmation_required" : response.status,
|
|
84
|
+
signerAddress: response.signerAddress ?? response.signer?.address,
|
|
85
|
+
typedData: response.typedData ?? response.typed_data
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
function buildSignedResult(response, request) {
|
|
89
|
+
assertAddress(response.signerAddress, "signerAddress");
|
|
90
|
+
if (response.typedData) {
|
|
91
|
+
assertTypedDataMatchesRequest(
|
|
92
|
+
response.typedData,
|
|
93
|
+
request,
|
|
94
|
+
response.signerAddress
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
return {
|
|
98
|
+
signature: response.signature,
|
|
99
|
+
signerAddress: response.signerAddress,
|
|
100
|
+
typedData: response.typedData ?? buildPersonalServerRegistrationTypedData({
|
|
101
|
+
ownerAddress: response.signerAddress,
|
|
102
|
+
...request
|
|
103
|
+
}),
|
|
104
|
+
intent: ACCOUNT_PERSONAL_SERVER_REGISTRATION_INTENT
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
function assertTypedDataMatchesRequest(typedData, request, expectedSignerAddress) {
|
|
108
|
+
assertAddress(
|
|
109
|
+
typedData.message.ownerAddress,
|
|
110
|
+
"typedData.message.ownerAddress"
|
|
111
|
+
);
|
|
112
|
+
assertAddress(
|
|
113
|
+
typedData.message.serverAddress,
|
|
114
|
+
"typedData.message.serverAddress"
|
|
115
|
+
);
|
|
116
|
+
if (expectedSignerAddress && !sameAddress(typedData.message.ownerAddress, expectedSignerAddress)) {
|
|
117
|
+
throw new Error(
|
|
118
|
+
"Account typedData ownerAddress must match the expected signer address"
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
if (!sameAddress(typedData.message.serverAddress, request.serverAddress)) {
|
|
122
|
+
throw new Error(
|
|
123
|
+
"Account typedData serverAddress must match the requested serverAddress"
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
if (typedData.message.publicKey !== request.serverPublicKey) {
|
|
127
|
+
throw new Error(
|
|
128
|
+
"Account typedData publicKey must match the requested serverPublicKey"
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
if (typedData.message.serverUrl !== request.serverUrl) {
|
|
132
|
+
throw new Error(
|
|
133
|
+
"Account typedData serverUrl must match the requested serverUrl"
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
if (typedData.primaryType !== "ServerRegistration") {
|
|
137
|
+
throw new Error("Account typedData primaryType must be ServerRegistration");
|
|
138
|
+
}
|
|
139
|
+
if (JSON.stringify(typedData.types) !== JSON.stringify(SERVER_REGISTRATION_TYPES)) {
|
|
140
|
+
throw new Error("Account typedData types must be ServerRegistration types");
|
|
141
|
+
}
|
|
142
|
+
const expectedDomain = personalServerRegistrationDomain({
|
|
143
|
+
config: request.config,
|
|
144
|
+
chainId: request.chainId,
|
|
145
|
+
verifyingContract: request.verifyingContract
|
|
146
|
+
});
|
|
147
|
+
if (!domainsEqual(typedData.domain, expectedDomain)) {
|
|
148
|
+
throw new Error("Account typedData domain must match the requested domain");
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
function sameAddress(a, b) {
|
|
152
|
+
return a.toLowerCase() === b.toLowerCase();
|
|
153
|
+
}
|
|
154
|
+
function domainsEqual(a, b) {
|
|
155
|
+
if (!a || !b) {
|
|
156
|
+
return false;
|
|
157
|
+
}
|
|
158
|
+
return a.name === b.name && a.version === b.version && Number(a.chainId) === Number(b.chainId) && String(a.verifyingContract ?? "").toLowerCase() === String(b.verifyingContract ?? "").toLowerCase() && a.salt === b.salt;
|
|
159
|
+
}
|
|
160
|
+
async function signPersonalServerRegistrationWithAccount(config, request) {
|
|
161
|
+
assertAddress(request.serverAddress, "serverAddress");
|
|
162
|
+
const fetchImpl = config.fetchImpl ?? globalThis.fetch.bind(globalThis);
|
|
163
|
+
const endpoint = new URL(
|
|
164
|
+
config.endpointPath ?? DEFAULT_ACCOUNT_PS_REGISTRATION_PATH,
|
|
165
|
+
`${trimTrailingSlash(config.accountOrigin)}/`
|
|
166
|
+
);
|
|
167
|
+
const response = await fetchImpl(endpoint, {
|
|
168
|
+
method: "POST",
|
|
169
|
+
headers: { "content-type": "application/json" },
|
|
170
|
+
credentials: "include",
|
|
171
|
+
body: JSON.stringify({
|
|
172
|
+
intent: ACCOUNT_PERSONAL_SERVER_REGISTRATION_INTENT,
|
|
173
|
+
serverAddress: request.serverAddress,
|
|
174
|
+
serverPublicKey: request.serverPublicKey,
|
|
175
|
+
serverUrl: request.serverUrl,
|
|
176
|
+
config: request.config,
|
|
177
|
+
chainId: request.chainId,
|
|
178
|
+
verifyingContract: request.verifyingContract
|
|
179
|
+
})
|
|
180
|
+
});
|
|
181
|
+
const body = normalizeAccountResponse(await parseAccountResponse(response));
|
|
182
|
+
if (body.status === "signed") {
|
|
183
|
+
if (!body.signature || !body.signerAddress) {
|
|
184
|
+
throw new Error(
|
|
185
|
+
"Account signed response must include signature and signerAddress"
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
return {
|
|
189
|
+
status: "signed",
|
|
190
|
+
result: buildSignedResult(
|
|
191
|
+
{
|
|
192
|
+
signature: body.signature,
|
|
193
|
+
signerAddress: body.signerAddress,
|
|
194
|
+
typedData: body.typedData
|
|
195
|
+
},
|
|
196
|
+
request
|
|
197
|
+
)
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
if (body.status === "confirmation_required") {
|
|
201
|
+
if (!body.typedData) {
|
|
202
|
+
throw new Error(
|
|
203
|
+
"Account confirmation_required response must include typedData"
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
assertTypedDataMatchesRequest(body.typedData, request, body.signerAddress);
|
|
207
|
+
if (!config.fallbackSigner) {
|
|
208
|
+
return {
|
|
209
|
+
status: "confirmation_required",
|
|
210
|
+
typedData: body.typedData,
|
|
211
|
+
signerAddress: body.signerAddress
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
assertTypedDataMatchesRequest(
|
|
215
|
+
body.typedData,
|
|
216
|
+
request,
|
|
217
|
+
config.fallbackSigner.address
|
|
218
|
+
);
|
|
219
|
+
const signature = await config.fallbackSigner.signTypedData(body.typedData);
|
|
220
|
+
return {
|
|
221
|
+
status: "fallback_signed",
|
|
222
|
+
accountStatus: "confirmation_required",
|
|
223
|
+
result: {
|
|
224
|
+
signature,
|
|
225
|
+
signerAddress: config.fallbackSigner.address,
|
|
226
|
+
typedData: body.typedData,
|
|
227
|
+
intent: ACCOUNT_PERSONAL_SERVER_REGISTRATION_INTENT
|
|
228
|
+
}
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
throw new Error(
|
|
232
|
+
`Unsupported Account PS registration signing status: ${String(body.status)}`
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
export {
|
|
236
|
+
ACCOUNT_PERSONAL_SERVER_REGISTRATION_INTENT,
|
|
237
|
+
AccountPersonalServerRegistrationError,
|
|
238
|
+
signPersonalServerRegistrationWithAccount
|
|
239
|
+
};
|
|
240
|
+
//# sourceMappingURL=personal-server-registration.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/account/personal-server-registration.ts"],"sourcesContent":["/**\n * Optional first-party Account integration for Personal Server registration.\n *\n * The protocol helper lives in `protocol/personal-server-registration`.\n * This module is only for callers that want to use an Account deployment's\n * constrained silent-sign endpoint.\n *\n * @category Account\n */\n\nimport { isAddress, type Address, type Hex } from \"viem\";\nimport {\n buildPersonalServerRegistrationTypedData,\n type BuildPersonalServerRegistrationTypedDataInput,\n type PersonalServerRegistrationSignature,\n type PersonalServerRegistrationSigner,\n type PersonalServerRegistrationTypedData,\n personalServerRegistrationDomain,\n} from \"../protocol/personal-server-registration\";\nimport { SERVER_REGISTRATION_TYPES } from \"../protocol/eip712\";\n\nexport const ACCOUNT_PERSONAL_SERVER_REGISTRATION_INTENT =\n \"personal_server.server_registration.v1\" as const;\n\nexport type AccountPersonalServerRegistrationIntent =\n typeof ACCOUNT_PERSONAL_SERVER_REGISTRATION_INTENT;\n\nexport type AccountPersonalServerRegistrationStatus =\n | \"signed\"\n | \"confirmation_required\"\n | \"fallback_required\";\n\nexport interface AccountPersonalServerRegistrationRequest extends Omit<\n BuildPersonalServerRegistrationTypedDataInput,\n \"ownerAddress\"\n> {}\n\nexport interface AccountPersonalServerRegistrationConfig {\n /**\n * Origin for the Account deployment to call, e.g. an app-dev Account origin.\n * No production origin is assumed by the SDK.\n */\n accountOrigin: string;\n /**\n * Path for Account's constrained PS registration silent-sign endpoint.\n */\n endpointPath?: string;\n /**\n * Optional fetch implementation for tests and non-default runtimes.\n */\n fetchImpl?: typeof fetch;\n /**\n * Optional signer used when Account says user confirmation is required and\n * returns typed data for the caller to sign interactively.\n */\n fallbackSigner?: PersonalServerRegistrationSigner;\n}\n\nexport type AccountPersonalServerRegistrationSignature =\n PersonalServerRegistrationSignature & {\n intent: AccountPersonalServerRegistrationIntent;\n };\n\nexport interface AccountSignedPersonalServerRegistration {\n status: \"signed\";\n result: AccountPersonalServerRegistrationSignature;\n}\n\nexport interface AccountConfirmationRequiredPersonalServerRegistration {\n status: \"confirmation_required\";\n typedData: PersonalServerRegistrationTypedData;\n signerAddress?: Address;\n}\n\nexport interface AccountFallbackSignedPersonalServerRegistration {\n status: \"fallback_signed\";\n accountStatus: \"confirmation_required\";\n result: AccountPersonalServerRegistrationSignature;\n}\n\nexport type AccountPersonalServerRegistrationResult =\n | AccountSignedPersonalServerRegistration\n | AccountConfirmationRequiredPersonalServerRegistration\n | AccountFallbackSignedPersonalServerRegistration;\n\nexport class AccountPersonalServerRegistrationError extends Error {\n status: number;\n code?: string;\n details?: unknown;\n\n constructor(input: {\n status: number;\n message: string;\n code?: string;\n details?: unknown;\n }) {\n super(input.message);\n this.name = \"AccountPersonalServerRegistrationError\";\n this.status = input.status;\n this.code = input.code;\n this.details = input.details;\n }\n}\n\ninterface AccountSilentSignResponse {\n status: AccountPersonalServerRegistrationStatus;\n signature?: Hex;\n signerAddress?: Address;\n signer?: { address?: Address };\n typedData?: PersonalServerRegistrationTypedData;\n typed_data?: PersonalServerRegistrationTypedData;\n error?: unknown;\n}\n\n// Account-owned route policy. Protocol signing primitives deliberately do not\n// define Account intent names or API paths.\nconst DEFAULT_ACCOUNT_PS_REGISTRATION_PATH =\n \"/api/v1/intents/personal-server-registration/sign\";\n\nfunction trimTrailingSlash(value: string): string {\n return value.replace(/\\/+$/, \"\");\n}\n\nfunction assertAddress(value: Address, name: string): void {\n if (!isAddress(value)) {\n throw new Error(`${name} must be a valid EVM address`);\n }\n}\n\nasync function parseAccountResponse(\n response: Response,\n): Promise<AccountSilentSignResponse> {\n const body = (await response.json().catch(() => undefined)) as unknown;\n\n if (!response.ok) {\n throw new AccountPersonalServerRegistrationError({\n status: response.status,\n code: accountErrorCode(body),\n message: accountErrorMessage(response.status, body),\n details: body,\n });\n }\n\n return body as AccountSilentSignResponse;\n}\n\nfunction accountErrorMessage(status: number, body: unknown): string {\n const nestedMessage = nestedAccountErrorField(body, \"message\");\n if (nestedMessage) {\n return nestedMessage;\n }\n\n if (isRecord(body) && typeof body.message === \"string\") {\n return body.message;\n }\n\n const code = accountErrorCode(body);\n if (code) {\n return `Account PS registration signing failed: ${code}`;\n }\n\n return `Account PS registration signing failed: ${status}`;\n}\n\nfunction accountErrorCode(body: unknown): string | undefined {\n const nestedCode = nestedAccountErrorField(body, \"code\");\n if (nestedCode) {\n return nestedCode;\n }\n\n if (isRecord(body)) {\n if (typeof body.code === \"string\") {\n return body.code;\n }\n if (typeof body.error === \"string\") {\n return body.error;\n }\n }\n\n return undefined;\n}\n\nfunction nestedAccountErrorField(\n body: unknown,\n field: \"code\" | \"message\",\n): string | undefined {\n if (!isRecord(body) || !isRecord(body.error)) {\n return undefined;\n }\n\n const value = body.error[field];\n return typeof value === \"string\" ? value : undefined;\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === \"object\" && value !== null;\n}\n\nfunction normalizeAccountResponse(\n response: AccountSilentSignResponse,\n): AccountSilentSignResponse {\n return {\n ...response,\n status:\n response.status === \"fallback_required\"\n ? \"confirmation_required\"\n : response.status,\n signerAddress: response.signerAddress ?? response.signer?.address,\n typedData: response.typedData ?? response.typed_data,\n };\n}\n\nfunction buildSignedResult(\n response: Required<\n Pick<AccountSilentSignResponse, \"signature\" | \"signerAddress\">\n > &\n Pick<AccountSilentSignResponse, \"typedData\">,\n request: AccountPersonalServerRegistrationRequest,\n): AccountPersonalServerRegistrationSignature {\n assertAddress(response.signerAddress, \"signerAddress\");\n if (response.typedData) {\n assertTypedDataMatchesRequest(\n response.typedData,\n request,\n response.signerAddress,\n );\n }\n\n return {\n signature: response.signature,\n signerAddress: response.signerAddress,\n typedData:\n response.typedData ??\n buildPersonalServerRegistrationTypedData({\n ownerAddress: response.signerAddress,\n ...request,\n }),\n intent: ACCOUNT_PERSONAL_SERVER_REGISTRATION_INTENT,\n };\n}\n\nfunction assertTypedDataMatchesRequest(\n typedData: PersonalServerRegistrationTypedData,\n request: AccountPersonalServerRegistrationRequest,\n expectedSignerAddress?: Address,\n): void {\n assertAddress(\n typedData.message.ownerAddress,\n \"typedData.message.ownerAddress\",\n );\n assertAddress(\n typedData.message.serverAddress,\n \"typedData.message.serverAddress\",\n );\n\n if (\n expectedSignerAddress &&\n !sameAddress(typedData.message.ownerAddress, expectedSignerAddress)\n ) {\n throw new Error(\n \"Account typedData ownerAddress must match the expected signer address\",\n );\n }\n\n if (!sameAddress(typedData.message.serverAddress, request.serverAddress)) {\n throw new Error(\n \"Account typedData serverAddress must match the requested serverAddress\",\n );\n }\n\n if (typedData.message.publicKey !== request.serverPublicKey) {\n throw new Error(\n \"Account typedData publicKey must match the requested serverPublicKey\",\n );\n }\n\n if (typedData.message.serverUrl !== request.serverUrl) {\n throw new Error(\n \"Account typedData serverUrl must match the requested serverUrl\",\n );\n }\n\n if (typedData.primaryType !== \"ServerRegistration\") {\n throw new Error(\"Account typedData primaryType must be ServerRegistration\");\n }\n\n if (\n JSON.stringify(typedData.types) !==\n JSON.stringify(SERVER_REGISTRATION_TYPES)\n ) {\n throw new Error(\"Account typedData types must be ServerRegistration types\");\n }\n\n const expectedDomain = personalServerRegistrationDomain({\n config: request.config,\n chainId: request.chainId,\n verifyingContract: request.verifyingContract,\n });\n if (!domainsEqual(typedData.domain, expectedDomain)) {\n throw new Error(\"Account typedData domain must match the requested domain\");\n }\n}\n\nfunction sameAddress(a: Address, b: Address): boolean {\n return a.toLowerCase() === b.toLowerCase();\n}\n\nfunction domainsEqual(\n a: PersonalServerRegistrationTypedData[\"domain\"],\n b: PersonalServerRegistrationTypedData[\"domain\"],\n): boolean {\n if (!a || !b) {\n return false;\n }\n\n return (\n a.name === b.name &&\n a.version === b.version &&\n Number(a.chainId) === Number(b.chainId) &&\n String(a.verifyingContract ?? \"\").toLowerCase() ===\n String(b.verifyingContract ?? \"\").toLowerCase() &&\n a.salt === b.salt\n );\n}\n\nexport async function signPersonalServerRegistrationWithAccount(\n config: AccountPersonalServerRegistrationConfig,\n request: AccountPersonalServerRegistrationRequest,\n): Promise<AccountPersonalServerRegistrationResult> {\n assertAddress(request.serverAddress, \"serverAddress\");\n\n const fetchImpl = config.fetchImpl ?? globalThis.fetch.bind(globalThis);\n const endpoint = new URL(\n config.endpointPath ?? DEFAULT_ACCOUNT_PS_REGISTRATION_PATH,\n `${trimTrailingSlash(config.accountOrigin)}/`,\n );\n\n const response = await fetchImpl(endpoint, {\n method: \"POST\",\n headers: { \"content-type\": \"application/json\" },\n credentials: \"include\",\n body: JSON.stringify({\n intent: ACCOUNT_PERSONAL_SERVER_REGISTRATION_INTENT,\n serverAddress: request.serverAddress,\n serverPublicKey: request.serverPublicKey,\n serverUrl: request.serverUrl,\n config: request.config,\n chainId: request.chainId,\n verifyingContract: request.verifyingContract,\n }),\n });\n const body = normalizeAccountResponse(await parseAccountResponse(response));\n\n if (body.status === \"signed\") {\n if (!body.signature || !body.signerAddress) {\n throw new Error(\n \"Account signed response must include signature and signerAddress\",\n );\n }\n\n return {\n status: \"signed\",\n result: buildSignedResult(\n {\n signature: body.signature,\n signerAddress: body.signerAddress,\n typedData: body.typedData,\n },\n request,\n ),\n };\n }\n\n if (body.status === \"confirmation_required\") {\n if (!body.typedData) {\n throw new Error(\n \"Account confirmation_required response must include typedData\",\n );\n }\n assertTypedDataMatchesRequest(body.typedData, request, body.signerAddress);\n\n if (!config.fallbackSigner) {\n return {\n status: \"confirmation_required\",\n typedData: body.typedData,\n signerAddress: body.signerAddress,\n };\n }\n\n assertTypedDataMatchesRequest(\n body.typedData,\n request,\n config.fallbackSigner.address,\n );\n const signature = await config.fallbackSigner.signTypedData(body.typedData);\n\n return {\n status: \"fallback_signed\",\n accountStatus: \"confirmation_required\",\n result: {\n signature,\n signerAddress: config.fallbackSigner.address,\n typedData: body.typedData,\n intent: ACCOUNT_PERSONAL_SERVER_REGISTRATION_INTENT,\n },\n };\n }\n\n throw new Error(\n `Unsupported Account PS registration signing status: ${String(body.status)}`,\n );\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/auth/errors.js
CHANGED
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
var oauth_client_exports = {};
|
|
20
|
+
__export(oauth_client_exports, {
|
|
21
|
+
OAuthClient: () => OAuthClient
|
|
22
|
+
});
|
|
23
|
+
module.exports = __toCommonJS(oauth_client_exports);
|
|
24
|
+
var import_pkce = require("./pkce");
|
|
25
|
+
var import_token_store = require("./token-store");
|
|
26
|
+
const VERIFIER_TTL_SECONDS = 600;
|
|
27
|
+
const RESERVED_AUTHORIZE_PARAMS = /* @__PURE__ */ new Set([
|
|
28
|
+
"response_type",
|
|
29
|
+
"client_id",
|
|
30
|
+
"redirect_uri",
|
|
31
|
+
"scope",
|
|
32
|
+
"state",
|
|
33
|
+
"code_challenge",
|
|
34
|
+
"code_challenge_method"
|
|
35
|
+
]);
|
|
36
|
+
class OAuthClient {
|
|
37
|
+
#config;
|
|
38
|
+
constructor(config) {
|
|
39
|
+
const fetchImpl = config.fetchImpl ?? globalThis.fetch;
|
|
40
|
+
if (typeof fetchImpl !== "function") {
|
|
41
|
+
throw new TypeError(
|
|
42
|
+
"OAuthClient requires a global `fetch` or an explicit `fetchImpl`"
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
this.#config = {
|
|
46
|
+
authorizationEndpoint: config.authorizationEndpoint,
|
|
47
|
+
tokenEndpoint: config.tokenEndpoint,
|
|
48
|
+
clientId: config.clientId,
|
|
49
|
+
redirectUri: config.redirectUri,
|
|
50
|
+
scope: config.scope,
|
|
51
|
+
tokenStore: config.tokenStore ?? new import_token_store.InMemoryTokenStore(),
|
|
52
|
+
fetchImpl,
|
|
53
|
+
generateState: config.generateState ?? defaultGenerateState
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
/** Build the authorize URL and persist the PKCE verifier keyed by `state`. */
|
|
57
|
+
async buildAuthorizationUrl(opts = {}) {
|
|
58
|
+
const state = opts.state ?? this.#config.generateState();
|
|
59
|
+
const scope = opts.scope ?? this.#config.scope;
|
|
60
|
+
const verifier = (0, import_pkce.generatePkceVerifier)();
|
|
61
|
+
const challenge = await (0, import_pkce.computePkceChallenge)(verifier);
|
|
62
|
+
await this.#config.tokenStore.set(this.#verifierKey(state), {
|
|
63
|
+
token: verifier,
|
|
64
|
+
expiresAt: Math.floor(Date.now() / 1e3) + VERIFIER_TTL_SECONDS
|
|
65
|
+
});
|
|
66
|
+
const params = new URLSearchParams();
|
|
67
|
+
params.set("response_type", "code");
|
|
68
|
+
params.set("client_id", this.#config.clientId);
|
|
69
|
+
params.set("redirect_uri", this.#config.redirectUri);
|
|
70
|
+
if (scope !== void 0 && scope.length > 0) {
|
|
71
|
+
params.set("scope", scope);
|
|
72
|
+
}
|
|
73
|
+
params.set("state", state);
|
|
74
|
+
params.set("code_challenge", challenge);
|
|
75
|
+
params.set("code_challenge_method", "S256");
|
|
76
|
+
if (opts.extraParams !== void 0) {
|
|
77
|
+
for (const k of Object.keys(opts.extraParams)) {
|
|
78
|
+
if (RESERVED_AUTHORIZE_PARAMS.has(k)) {
|
|
79
|
+
throw new Error(
|
|
80
|
+
`extraParams may not override the reserved OAuth/PKCE parameter "${k}"`
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
for (const [k, v] of Object.entries(opts.extraParams)) {
|
|
85
|
+
params.set(k, v);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
const sep = this.#config.authorizationEndpoint.includes("?") ? "&" : "?";
|
|
89
|
+
const url = `${this.#config.authorizationEndpoint}${sep}${params.toString()}`;
|
|
90
|
+
return { url, state };
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Handle the redirect-callback URL. Validates `state`, retrieves the saved
|
|
94
|
+
* verifier, exchanges the authorization code + verifier for tokens, and
|
|
95
|
+
* persists them. Returns the access {@link TokenRecord}.
|
|
96
|
+
*/
|
|
97
|
+
async handleCallback(callbackUrl) {
|
|
98
|
+
const parsed = new URL(callbackUrl);
|
|
99
|
+
const params = parsed.searchParams;
|
|
100
|
+
const errorCode = params.get("error");
|
|
101
|
+
if (errorCode !== null) {
|
|
102
|
+
throw new Error(
|
|
103
|
+
formatOAuthError({
|
|
104
|
+
error: errorCode,
|
|
105
|
+
error_description: params.get("error_description") ?? void 0
|
|
106
|
+
})
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
const code = params.get("code");
|
|
110
|
+
const state = params.get("state");
|
|
111
|
+
if (code === null || state === null) {
|
|
112
|
+
throw new Error("OAuth callback is missing `code` or `state`");
|
|
113
|
+
}
|
|
114
|
+
const verifierRecord = await this.#config.tokenStore.get(
|
|
115
|
+
this.#verifierKey(state)
|
|
116
|
+
);
|
|
117
|
+
if (verifierRecord === null) {
|
|
118
|
+
throw new Error(
|
|
119
|
+
"OAuth callback state does not match any in-flight verifier (possible CSRF or expired flow)"
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
const body = new URLSearchParams();
|
|
123
|
+
body.set("grant_type", "authorization_code");
|
|
124
|
+
body.set("code", code);
|
|
125
|
+
body.set("redirect_uri", this.#config.redirectUri);
|
|
126
|
+
body.set("client_id", this.#config.clientId);
|
|
127
|
+
body.set("code_verifier", verifierRecord.token);
|
|
128
|
+
let tokens;
|
|
129
|
+
try {
|
|
130
|
+
tokens = await this.#tokenRequest(body);
|
|
131
|
+
} finally {
|
|
132
|
+
await this.#config.tokenStore.delete(this.#verifierKey(state));
|
|
133
|
+
}
|
|
134
|
+
return this.#persistTokens(tokens);
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Exchange a stored refresh token for a fresh access token. Throws if no
|
|
138
|
+
* refresh token is available.
|
|
139
|
+
*/
|
|
140
|
+
async refresh() {
|
|
141
|
+
const refreshRecord = await this.#config.tokenStore.get(this.#refreshKey());
|
|
142
|
+
if (refreshRecord === null) {
|
|
143
|
+
throw new Error("OAuth refresh failed: no refresh token stored");
|
|
144
|
+
}
|
|
145
|
+
const body = new URLSearchParams();
|
|
146
|
+
body.set("grant_type", "refresh_token");
|
|
147
|
+
body.set("refresh_token", refreshRecord.token);
|
|
148
|
+
body.set("client_id", this.#config.clientId);
|
|
149
|
+
const tokens = await this.#tokenRequest(body);
|
|
150
|
+
return this.#persistTokens(tokens, refreshRecord.token);
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Get the current access token if valid (refreshing first if expired and a
|
|
154
|
+
* refresh token is available). Returns `null` when no usable token exists.
|
|
155
|
+
*/
|
|
156
|
+
async getAccessToken() {
|
|
157
|
+
const stored = await this.#config.tokenStore.get(this.#accessKey());
|
|
158
|
+
if (stored !== null) return stored.token;
|
|
159
|
+
const refresh = await this.#config.tokenStore.get(this.#refreshKey());
|
|
160
|
+
if (refresh === null) return null;
|
|
161
|
+
try {
|
|
162
|
+
const refreshed = await this.refresh();
|
|
163
|
+
return refreshed.token;
|
|
164
|
+
} catch {
|
|
165
|
+
return null;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
/** Forget tokens (logout). Does NOT call any remote revocation endpoint. */
|
|
169
|
+
async signOut() {
|
|
170
|
+
await this.#config.tokenStore.delete(this.#accessKey());
|
|
171
|
+
await this.#config.tokenStore.delete(this.#refreshKey());
|
|
172
|
+
}
|
|
173
|
+
#accessKey() {
|
|
174
|
+
return `oauth:tokens:${this.#config.clientId}`;
|
|
175
|
+
}
|
|
176
|
+
#refreshKey() {
|
|
177
|
+
return `oauth:refresh:${this.#config.clientId}`;
|
|
178
|
+
}
|
|
179
|
+
#verifierKey(state) {
|
|
180
|
+
return `oauth:verifier:${state}`;
|
|
181
|
+
}
|
|
182
|
+
async #tokenRequest(body) {
|
|
183
|
+
const response = await this.#config.fetchImpl(this.#config.tokenEndpoint, {
|
|
184
|
+
method: "POST",
|
|
185
|
+
headers: {
|
|
186
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
187
|
+
Accept: "application/json"
|
|
188
|
+
},
|
|
189
|
+
body: body.toString()
|
|
190
|
+
});
|
|
191
|
+
const text = await response.text();
|
|
192
|
+
const parsed = parseJsonBody(text);
|
|
193
|
+
if (!response.ok) {
|
|
194
|
+
throw new Error(formatOAuthError(parsed ?? {}, response.status));
|
|
195
|
+
}
|
|
196
|
+
if (parsed === null || typeof parsed !== "object" || typeof parsed.access_token !== "string") {
|
|
197
|
+
throw new Error(
|
|
198
|
+
"OAuth token endpoint returned a response without an `access_token` string"
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
return parsed;
|
|
202
|
+
}
|
|
203
|
+
async #persistTokens(tokens, previousRefreshToken) {
|
|
204
|
+
const record = { token: tokens.access_token };
|
|
205
|
+
if (typeof tokens.expires_in === "number" && tokens.expires_in > 0) {
|
|
206
|
+
record.expiresAt = Math.floor(Date.now() / 1e3) + tokens.expires_in;
|
|
207
|
+
}
|
|
208
|
+
await this.#config.tokenStore.set(this.#accessKey(), record);
|
|
209
|
+
const newRefresh = tokens.refresh_token ?? previousRefreshToken;
|
|
210
|
+
if (newRefresh !== void 0) {
|
|
211
|
+
await this.#config.tokenStore.set(this.#refreshKey(), {
|
|
212
|
+
token: newRefresh
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
return record;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
function defaultGenerateState() {
|
|
219
|
+
const bytes = new Uint8Array(24);
|
|
220
|
+
crypto.getRandomValues(bytes);
|
|
221
|
+
let binary = "";
|
|
222
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
223
|
+
binary += String.fromCharCode(bytes[i]);
|
|
224
|
+
}
|
|
225
|
+
return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
226
|
+
}
|
|
227
|
+
function parseJsonBody(text) {
|
|
228
|
+
if (text.length === 0) return null;
|
|
229
|
+
try {
|
|
230
|
+
return JSON.parse(text);
|
|
231
|
+
} catch {
|
|
232
|
+
return null;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
function formatOAuthError(body, status) {
|
|
236
|
+
const parts = ["OAuth token request failed"];
|
|
237
|
+
if (status !== void 0) parts.push(`(HTTP ${String(status)})`);
|
|
238
|
+
if (body.error !== void 0 && body.error.length > 0) {
|
|
239
|
+
parts.push(`: ${body.error}`);
|
|
240
|
+
if (body.error_description !== void 0 && body.error_description.length > 0) {
|
|
241
|
+
parts.push(`- ${body.error_description}`);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
return parts.join(" ").replace(" : ", ": ").replace(" - ", " - ");
|
|
245
|
+
}
|
|
246
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
247
|
+
0 && (module.exports = {
|
|
248
|
+
OAuthClient
|
|
249
|
+
});
|
|
250
|
+
//# sourceMappingURL=oauth-client.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/auth/oauth-client.ts"],"sourcesContent":["/**\n * OAuth 2.0 Authorization Code + PKCE client orchestration.\n *\n * @remarks\n * Drives the full authorize → callback → token-exchange → refresh dance on top\n * of the {@link TokenStore} and PKCE primitives that ship with this package.\n * Implements RFC 6749 §4.1 with the RFC 7636 PKCE extension (S256 only).\n *\n * @category Auth\n * @module auth/oauth-client\n */\n\nimport { computePkceChallenge, generatePkceVerifier } from \"./pkce\";\nimport {\n InMemoryTokenStore,\n type TokenRecord,\n type TokenStore,\n} from \"./token-store\";\n\n/**\n * Constructor options for {@link OAuthClient}.\n */\nexport interface OAuthClientConfig {\n /** Authorization endpoint, e.g. `https://account.vana.org/oauth/authorize`. */\n authorizationEndpoint: string;\n /** Token endpoint, e.g. `https://account.vana.org/oauth/token`. */\n tokenEndpoint: string;\n /** OAuth `client_id` (public; PKCE protects the flow). */\n clientId: string;\n /** Redirect URI registered with the authorization server. */\n redirectUri: string;\n /** Default scope; can be overridden per call. */\n scope?: string;\n /**\n * Where to persist access + refresh tokens and the in-flight code verifier\n * between `authorize` → `callback`. Defaults to a fresh\n * {@link InMemoryTokenStore}. Use IndexedDB/localStorage-backed\n * implementations for browser apps where the user navigates away during the\n * dance.\n */\n tokenStore?: TokenStore;\n /** Override the global `fetch` (e.g. for tests). Defaults to `globalThis.fetch`. */\n fetchImpl?: typeof fetch;\n /**\n * Override the random-state generator (mostly for tests). Must return a\n * URL-safe string of >= 16 bytes of entropy.\n */\n generateState?: () => string;\n}\n\n/**\n * Result of {@link OAuthClient.buildAuthorizationUrl}.\n */\nexport interface AuthorizationUrlResult {\n /** The full authorize URL to redirect / `window.open` to. */\n url: string;\n /** The `state` value the auth server will echo back; used for CSRF check. */\n state: string;\n}\n\n/** TTL for the in-flight verifier record (seconds). */\nconst VERIFIER_TTL_SECONDS = 600;\n\n/** RFC 6749 spec-compliant OAuth error payload shape. */\ninterface OAuthErrorBody {\n error?: string;\n error_description?: string;\n error_uri?: string;\n}\n\n/** Successful token-endpoint response shape (RFC 6749 §5.1). */\ninterface TokenEndpointResponse {\n access_token: string;\n token_type?: string;\n expires_in?: number;\n refresh_token?: string;\n scope?: string;\n}\n\n/**\n * Authorize-URL parameters the client owns. Callers may NOT supply these\n * via `extraParams` — otherwise PKCE/CSRF protection can be silently\n * bypassed (e.g. `extraParams: { state: \"x\" }` would store the verifier\n * under the generated state but send `x` on the wire, breaking the\n * callback CSRF check; `code_challenge_method` could downgrade S256).\n */\nconst RESERVED_AUTHORIZE_PARAMS = new Set([\n \"response_type\",\n \"client_id\",\n \"redirect_uri\",\n \"scope\",\n \"state\",\n \"code_challenge\",\n \"code_challenge_method\",\n]);\n\n/**\n * OAuth 2.0 Authorization Code + PKCE client.\n *\n * @remarks\n * Storage layout under the supplied {@link TokenStore} (all keys namespaced):\n * - `oauth:tokens:{clientId}` → access token record\n * - `oauth:refresh:{clientId}` → refresh token record (no expiry)\n * - `oauth:verifier:{state}` → in-flight PKCE verifier (10 min TTL)\n *\n * @category Auth\n */\nexport class OAuthClient {\n readonly #config: Required<\n Omit<\n OAuthClientConfig,\n \"scope\" | \"tokenStore\" | \"fetchImpl\" | \"generateState\"\n >\n > & {\n scope?: string;\n tokenStore: TokenStore;\n fetchImpl: typeof fetch;\n generateState: () => string;\n };\n\n public constructor(config: OAuthClientConfig) {\n const fetchImpl = config.fetchImpl ?? globalThis.fetch;\n if (typeof fetchImpl !== \"function\") {\n throw new TypeError(\n \"OAuthClient requires a global `fetch` or an explicit `fetchImpl`\",\n );\n }\n\n this.#config = {\n authorizationEndpoint: config.authorizationEndpoint,\n tokenEndpoint: config.tokenEndpoint,\n clientId: config.clientId,\n redirectUri: config.redirectUri,\n scope: config.scope,\n tokenStore: config.tokenStore ?? new InMemoryTokenStore(),\n fetchImpl,\n generateState: config.generateState ?? defaultGenerateState,\n };\n }\n\n /** Build the authorize URL and persist the PKCE verifier keyed by `state`. */\n public async buildAuthorizationUrl(\n opts: {\n state?: string;\n scope?: string;\n extraParams?: Record<string, string>;\n } = {},\n ): Promise<AuthorizationUrlResult> {\n const state = opts.state ?? this.#config.generateState();\n const scope = opts.scope ?? this.#config.scope;\n\n const verifier = generatePkceVerifier();\n const challenge = await computePkceChallenge(verifier);\n\n await this.#config.tokenStore.set(this.#verifierKey(state), {\n token: verifier,\n expiresAt: Math.floor(Date.now() / 1000) + VERIFIER_TTL_SECONDS,\n });\n\n const params = new URLSearchParams();\n params.set(\"response_type\", \"code\");\n params.set(\"client_id\", this.#config.clientId);\n params.set(\"redirect_uri\", this.#config.redirectUri);\n if (scope !== undefined && scope.length > 0) {\n params.set(\"scope\", scope);\n }\n params.set(\"state\", state);\n params.set(\"code_challenge\", challenge);\n params.set(\"code_challenge_method\", \"S256\");\n if (opts.extraParams !== undefined) {\n for (const k of Object.keys(opts.extraParams)) {\n if (RESERVED_AUTHORIZE_PARAMS.has(k)) {\n throw new Error(\n `extraParams may not override the reserved OAuth/PKCE parameter \"${k}\"`,\n );\n }\n }\n for (const [k, v] of Object.entries(opts.extraParams)) {\n params.set(k, v);\n }\n }\n\n const sep = this.#config.authorizationEndpoint.includes(\"?\") ? \"&\" : \"?\";\n const url = `${this.#config.authorizationEndpoint}${sep}${params.toString()}`;\n\n return { url, state };\n }\n\n /**\n * Handle the redirect-callback URL. Validates `state`, retrieves the saved\n * verifier, exchanges the authorization code + verifier for tokens, and\n * persists them. Returns the access {@link TokenRecord}.\n */\n public async handleCallback(callbackUrl: string): Promise<TokenRecord> {\n const parsed = new URL(callbackUrl);\n const params = parsed.searchParams;\n\n const errorCode = params.get(\"error\");\n if (errorCode !== null) {\n throw new Error(\n formatOAuthError({\n error: errorCode,\n error_description: params.get(\"error_description\") ?? undefined,\n }),\n );\n }\n\n const code = params.get(\"code\");\n const state = params.get(\"state\");\n if (code === null || state === null) {\n throw new Error(\"OAuth callback is missing `code` or `state`\");\n }\n\n const verifierRecord = await this.#config.tokenStore.get(\n this.#verifierKey(state),\n );\n if (verifierRecord === null) {\n throw new Error(\n \"OAuth callback state does not match any in-flight verifier (possible CSRF or expired flow)\",\n );\n }\n\n const body = new URLSearchParams();\n body.set(\"grant_type\", \"authorization_code\");\n body.set(\"code\", code);\n body.set(\"redirect_uri\", this.#config.redirectUri);\n body.set(\"client_id\", this.#config.clientId);\n body.set(\"code_verifier\", verifierRecord.token);\n\n let tokens: TokenEndpointResponse;\n try {\n tokens = await this.#tokenRequest(body);\n } finally {\n // Always clear the one-shot verifier, even on a failed exchange.\n await this.#config.tokenStore.delete(this.#verifierKey(state));\n }\n\n return this.#persistTokens(tokens);\n }\n\n /**\n * Exchange a stored refresh token for a fresh access token. Throws if no\n * refresh token is available.\n */\n public async refresh(): Promise<TokenRecord> {\n const refreshRecord = await this.#config.tokenStore.get(this.#refreshKey());\n if (refreshRecord === null) {\n throw new Error(\"OAuth refresh failed: no refresh token stored\");\n }\n\n const body = new URLSearchParams();\n body.set(\"grant_type\", \"refresh_token\");\n body.set(\"refresh_token\", refreshRecord.token);\n body.set(\"client_id\", this.#config.clientId);\n\n const tokens = await this.#tokenRequest(body);\n return this.#persistTokens(tokens, refreshRecord.token);\n }\n\n /**\n * Get the current access token if valid (refreshing first if expired and a\n * refresh token is available). Returns `null` when no usable token exists.\n */\n public async getAccessToken(): Promise<string | null> {\n const stored = await this.#config.tokenStore.get(this.#accessKey());\n if (stored !== null) return stored.token;\n\n // Stored access token is missing or already evicted by the store's TTL.\n const refresh = await this.#config.tokenStore.get(this.#refreshKey());\n if (refresh === null) return null;\n\n try {\n const refreshed = await this.refresh();\n return refreshed.token;\n } catch {\n return null;\n }\n }\n\n /** Forget tokens (logout). Does NOT call any remote revocation endpoint. */\n public async signOut(): Promise<void> {\n await this.#config.tokenStore.delete(this.#accessKey());\n await this.#config.tokenStore.delete(this.#refreshKey());\n }\n\n #accessKey(): string {\n return `oauth:tokens:${this.#config.clientId}`;\n }\n\n #refreshKey(): string {\n return `oauth:refresh:${this.#config.clientId}`;\n }\n\n #verifierKey(state: string): string {\n return `oauth:verifier:${state}`;\n }\n\n async #tokenRequest(body: URLSearchParams): Promise<TokenEndpointResponse> {\n const response = await this.#config.fetchImpl(this.#config.tokenEndpoint, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/x-www-form-urlencoded\",\n Accept: \"application/json\",\n },\n body: body.toString(),\n });\n\n const text = await response.text();\n const parsed = parseJsonBody(text);\n\n if (!response.ok) {\n throw new Error(formatOAuthError(parsed ?? {}, response.status));\n }\n\n if (\n parsed === null ||\n typeof parsed !== \"object\" ||\n typeof (parsed as { access_token?: unknown }).access_token !== \"string\"\n ) {\n throw new Error(\n \"OAuth token endpoint returned a response without an `access_token` string\",\n );\n }\n\n return parsed as TokenEndpointResponse;\n }\n\n async #persistTokens(\n tokens: TokenEndpointResponse,\n previousRefreshToken?: string,\n ): Promise<TokenRecord> {\n const record: TokenRecord = { token: tokens.access_token };\n if (typeof tokens.expires_in === \"number\" && tokens.expires_in > 0) {\n record.expiresAt = Math.floor(Date.now() / 1000) + tokens.expires_in;\n }\n await this.#config.tokenStore.set(this.#accessKey(), record);\n\n const newRefresh = tokens.refresh_token ?? previousRefreshToken;\n if (newRefresh !== undefined) {\n await this.#config.tokenStore.set(this.#refreshKey(), {\n token: newRefresh,\n });\n }\n\n return record;\n }\n}\n\nfunction defaultGenerateState(): string {\n const bytes = new Uint8Array(24);\n crypto.getRandomValues(bytes);\n let binary = \"\";\n for (let i = 0; i < bytes.length; i++) {\n binary += String.fromCharCode(bytes[i] as number);\n }\n return btoa(binary)\n .replace(/\\+/g, \"-\")\n .replace(/\\//g, \"_\")\n .replace(/=+$/, \"\");\n}\n\nfunction parseJsonBody(text: string): unknown {\n if (text.length === 0) return null;\n try {\n return JSON.parse(text) as unknown;\n } catch {\n return null;\n }\n}\n\nfunction formatOAuthError(body: OAuthErrorBody, status?: number): string {\n const parts: string[] = [\"OAuth token request failed\"];\n if (status !== undefined) parts.push(`(HTTP ${String(status)})`);\n if (body.error !== undefined && body.error.length > 0) {\n parts.push(`: ${body.error}`);\n if (\n body.error_description !== undefined &&\n body.error_description.length > 0\n ) {\n parts.push(`- ${body.error_description}`);\n }\n }\n return parts.join(\" \").replace(\" : \", \": \").replace(\" - \", \" - \");\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OAuth 2.0 Authorization Code + PKCE client orchestration.
|
|
3
|
+
*
|
|
4
|
+
* @remarks
|
|
5
|
+
* Drives the full authorize → callback → token-exchange → refresh dance on top
|
|
6
|
+
* of the {@link TokenStore} and PKCE primitives that ship with this package.
|
|
7
|
+
* Implements RFC 6749 §4.1 with the RFC 7636 PKCE extension (S256 only).
|
|
8
|
+
*
|
|
9
|
+
* @category Auth
|
|
10
|
+
* @module auth/oauth-client
|
|
11
|
+
*/
|
|
12
|
+
import { type TokenRecord, type TokenStore } from "./token-store";
|
|
13
|
+
/**
|
|
14
|
+
* Constructor options for {@link OAuthClient}.
|
|
15
|
+
*/
|
|
16
|
+
export interface OAuthClientConfig {
|
|
17
|
+
/** Authorization endpoint, e.g. `https://account.vana.org/oauth/authorize`. */
|
|
18
|
+
authorizationEndpoint: string;
|
|
19
|
+
/** Token endpoint, e.g. `https://account.vana.org/oauth/token`. */
|
|
20
|
+
tokenEndpoint: string;
|
|
21
|
+
/** OAuth `client_id` (public; PKCE protects the flow). */
|
|
22
|
+
clientId: string;
|
|
23
|
+
/** Redirect URI registered with the authorization server. */
|
|
24
|
+
redirectUri: string;
|
|
25
|
+
/** Default scope; can be overridden per call. */
|
|
26
|
+
scope?: string;
|
|
27
|
+
/**
|
|
28
|
+
* Where to persist access + refresh tokens and the in-flight code verifier
|
|
29
|
+
* between `authorize` → `callback`. Defaults to a fresh
|
|
30
|
+
* {@link InMemoryTokenStore}. Use IndexedDB/localStorage-backed
|
|
31
|
+
* implementations for browser apps where the user navigates away during the
|
|
32
|
+
* dance.
|
|
33
|
+
*/
|
|
34
|
+
tokenStore?: TokenStore;
|
|
35
|
+
/** Override the global `fetch` (e.g. for tests). Defaults to `globalThis.fetch`. */
|
|
36
|
+
fetchImpl?: typeof fetch;
|
|
37
|
+
/**
|
|
38
|
+
* Override the random-state generator (mostly for tests). Must return a
|
|
39
|
+
* URL-safe string of >= 16 bytes of entropy.
|
|
40
|
+
*/
|
|
41
|
+
generateState?: () => string;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Result of {@link OAuthClient.buildAuthorizationUrl}.
|
|
45
|
+
*/
|
|
46
|
+
export interface AuthorizationUrlResult {
|
|
47
|
+
/** The full authorize URL to redirect / `window.open` to. */
|
|
48
|
+
url: string;
|
|
49
|
+
/** The `state` value the auth server will echo back; used for CSRF check. */
|
|
50
|
+
state: string;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* OAuth 2.0 Authorization Code + PKCE client.
|
|
54
|
+
*
|
|
55
|
+
* @remarks
|
|
56
|
+
* Storage layout under the supplied {@link TokenStore} (all keys namespaced):
|
|
57
|
+
* - `oauth:tokens:{clientId}` → access token record
|
|
58
|
+
* - `oauth:refresh:{clientId}` → refresh token record (no expiry)
|
|
59
|
+
* - `oauth:verifier:{state}` → in-flight PKCE verifier (10 min TTL)
|
|
60
|
+
*
|
|
61
|
+
* @category Auth
|
|
62
|
+
*/
|
|
63
|
+
export declare class OAuthClient {
|
|
64
|
+
#private;
|
|
65
|
+
constructor(config: OAuthClientConfig);
|
|
66
|
+
/** Build the authorize URL and persist the PKCE verifier keyed by `state`. */
|
|
67
|
+
buildAuthorizationUrl(opts?: {
|
|
68
|
+
state?: string;
|
|
69
|
+
scope?: string;
|
|
70
|
+
extraParams?: Record<string, string>;
|
|
71
|
+
}): Promise<AuthorizationUrlResult>;
|
|
72
|
+
/**
|
|
73
|
+
* Handle the redirect-callback URL. Validates `state`, retrieves the saved
|
|
74
|
+
* verifier, exchanges the authorization code + verifier for tokens, and
|
|
75
|
+
* persists them. Returns the access {@link TokenRecord}.
|
|
76
|
+
*/
|
|
77
|
+
handleCallback(callbackUrl: string): Promise<TokenRecord>;
|
|
78
|
+
/**
|
|
79
|
+
* Exchange a stored refresh token for a fresh access token. Throws if no
|
|
80
|
+
* refresh token is available.
|
|
81
|
+
*/
|
|
82
|
+
refresh(): Promise<TokenRecord>;
|
|
83
|
+
/**
|
|
84
|
+
* Get the current access token if valid (refreshing first if expired and a
|
|
85
|
+
* refresh token is available). Returns `null` when no usable token exists.
|
|
86
|
+
*/
|
|
87
|
+
getAccessToken(): Promise<string | null>;
|
|
88
|
+
/** Forget tokens (logout). Does NOT call any remote revocation endpoint. */
|
|
89
|
+
signOut(): Promise<void>;
|
|
90
|
+
}
|