viyv-db-auth 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.
- package/LICENSE +21 -0
- package/dist/credential-store.d.ts +9 -0
- package/dist/credential-store.d.ts.map +1 -0
- package/dist/credential-store.js +40 -0
- package/dist/credential-store.js.map +1 -0
- package/dist/device-code-auth.d.ts +13 -0
- package/dist/device-code-auth.d.ts.map +1 -0
- package/dist/device-code-auth.js +98 -0
- package/dist/device-code-auth.js.map +1 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -0
- package/dist/token-refresh.d.ts +11 -0
- package/dist/token-refresh.d.ts.map +1 -0
- package/dist/token-refresh.js +44 -0
- package/dist/token-refresh.js.map +1 -0
- package/dist/types.d.ts +48 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/package.json +36 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 viyv
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { ICredentialStore, StoredCredentials } from './types.js';
|
|
2
|
+
export declare class FileCredentialStore implements ICredentialStore {
|
|
3
|
+
private readonly filePath;
|
|
4
|
+
constructor(dir?: string);
|
|
5
|
+
load(): Promise<StoredCredentials | null>;
|
|
6
|
+
save(credentials: StoredCredentials): Promise<void>;
|
|
7
|
+
clear(): Promise<void>;
|
|
8
|
+
}
|
|
9
|
+
//# sourceMappingURL=credential-store.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"credential-store.d.ts","sourceRoot":"","sources":["../src/credential-store.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,gBAAgB,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AAKtE,qBAAa,mBAAoB,YAAW,gBAAgB;IAC3D,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAS;gBAEtB,GAAG,CAAC,EAAE,MAAM;IAIlB,IAAI,IAAI,OAAO,CAAC,iBAAiB,GAAG,IAAI,CAAC;IASzC,IAAI,CAAC,WAAW,EAAE,iBAAiB,GAAG,OAAO,CAAC,IAAI,CAAC;IAanD,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;CAO5B"}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { mkdir, readFile, rename, unlink, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { homedir } from 'node:os';
|
|
3
|
+
import { dirname, join } from 'node:path';
|
|
4
|
+
const DEFAULT_DIR = join(homedir(), '.viyv');
|
|
5
|
+
const DEFAULT_FILE = 'credentials.json';
|
|
6
|
+
export class FileCredentialStore {
|
|
7
|
+
filePath;
|
|
8
|
+
constructor(dir) {
|
|
9
|
+
this.filePath = join(dir ?? DEFAULT_DIR, DEFAULT_FILE);
|
|
10
|
+
}
|
|
11
|
+
async load() {
|
|
12
|
+
try {
|
|
13
|
+
const data = await readFile(this.filePath, 'utf-8');
|
|
14
|
+
return JSON.parse(data);
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
async save(credentials) {
|
|
21
|
+
const dir = dirname(this.filePath);
|
|
22
|
+
await mkdir(dir, { recursive: true, mode: 0o700 });
|
|
23
|
+
// Atomic write: write to tmp file then rename
|
|
24
|
+
const tmpPath = `${this.filePath}.tmp`;
|
|
25
|
+
await writeFile(tmpPath, JSON.stringify(credentials, null, '\t'), {
|
|
26
|
+
encoding: 'utf-8',
|
|
27
|
+
mode: 0o600,
|
|
28
|
+
});
|
|
29
|
+
await rename(tmpPath, this.filePath);
|
|
30
|
+
}
|
|
31
|
+
async clear() {
|
|
32
|
+
try {
|
|
33
|
+
await unlink(this.filePath);
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
// File may not exist, that's fine
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
//# sourceMappingURL=credential-store.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"credential-store.js","sourceRoot":"","sources":["../src/credential-store.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAC9E,OAAO,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAClC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAG1C,MAAM,WAAW,GAAG,IAAI,CAAC,OAAO,EAAE,EAAE,OAAO,CAAC,CAAC;AAC7C,MAAM,YAAY,GAAG,kBAAkB,CAAC;AAExC,MAAM,OAAO,mBAAmB;IACd,QAAQ,CAAS;IAElC,YAAY,GAAY;QACvB,IAAI,CAAC,QAAQ,GAAG,IAAI,CAAC,GAAG,IAAI,WAAW,EAAE,YAAY,CAAC,CAAC;IACxD,CAAC;IAED,KAAK,CAAC,IAAI;QACT,IAAI,CAAC;YACJ,MAAM,IAAI,GAAG,MAAM,QAAQ,CAAC,IAAI,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC;YACpD,OAAO,IAAI,CAAC,KAAK,CAAC,IAAI,CAAsB,CAAC;QAC9C,CAAC;QAAC,MAAM,CAAC;YACR,OAAO,IAAI,CAAC;QACb,CAAC;IACF,CAAC;IAED,KAAK,CAAC,IAAI,CAAC,WAA8B;QACxC,MAAM,GAAG,GAAG,OAAO,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;QACnC,MAAM,KAAK,CAAC,GAAG,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC;QAEnD,8CAA8C;QAC9C,MAAM,OAAO,GAAG,GAAG,IAAI,CAAC,QAAQ,MAAM,CAAC;QACvC,MAAM,SAAS,CAAC,OAAO,EAAE,IAAI,CAAC,SAAS,CAAC,WAAW,EAAE,IAAI,EAAE,IAAI,CAAC,EAAE;YACjE,QAAQ,EAAE,OAAO;YACjB,IAAI,EAAE,KAAK;SACX,CAAC,CAAC;QACH,MAAM,MAAM,CAAC,OAAO,EAAE,IAAI,CAAC,QAAQ,CAAC,CAAC;IACtC,CAAC;IAED,KAAK,CAAC,KAAK;QACV,IAAI,CAAC;YACJ,MAAM,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;QAC7B,CAAC;QAAC,MAAM,CAAC;YACR,kCAAkC;QACnC,CAAC;IACF,CAAC;CACD"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { ICredentialStore, UserInfo } from './types.js';
|
|
2
|
+
export declare class DeviceCodeAuth {
|
|
3
|
+
private readonly serverUrl;
|
|
4
|
+
private readonly store;
|
|
5
|
+
constructor(serverUrl: string, store: ICredentialStore);
|
|
6
|
+
login(): Promise<{
|
|
7
|
+
user: UserInfo;
|
|
8
|
+
}>;
|
|
9
|
+
private openBrowser;
|
|
10
|
+
private pollForToken;
|
|
11
|
+
private fetchUserInfo;
|
|
12
|
+
}
|
|
13
|
+
//# sourceMappingURL=device-code-auth.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"device-code-auth.d.ts","sourceRoot":"","sources":["../src/device-code-auth.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAEX,gBAAgB,EAIhB,QAAQ,EAER,MAAM,YAAY,CAAC;AAEpB,qBAAa,cAAc;IAEzB,OAAO,CAAC,QAAQ,CAAC,SAAS;IAC1B,OAAO,CAAC,QAAQ,CAAC,KAAK;gBADL,SAAS,EAAE,MAAM,EACjB,KAAK,EAAE,gBAAgB;IAGnC,KAAK,IAAI,OAAO,CAAC;QAAE,IAAI,EAAE,QAAQ,CAAA;KAAE,CAAC;IAsC1C,OAAO,CAAC,WAAW;YASL,YAAY;YA0CZ,aAAa;CAgB3B"}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { execFile } from 'node:child_process';
|
|
2
|
+
import { platform } from 'node:os';
|
|
3
|
+
export class DeviceCodeAuth {
|
|
4
|
+
serverUrl;
|
|
5
|
+
store;
|
|
6
|
+
constructor(serverUrl, store) {
|
|
7
|
+
this.serverUrl = serverUrl;
|
|
8
|
+
this.store = store;
|
|
9
|
+
}
|
|
10
|
+
async login() {
|
|
11
|
+
// 1. Request device code
|
|
12
|
+
const deviceRes = await fetch(`${this.serverUrl}/oauth/device/code`, {
|
|
13
|
+
method: 'POST',
|
|
14
|
+
headers: { 'Content-Type': 'application/json' },
|
|
15
|
+
});
|
|
16
|
+
if (!deviceRes.ok) {
|
|
17
|
+
throw new Error(`Device code request failed (${deviceRes.status})`);
|
|
18
|
+
}
|
|
19
|
+
const device = (await deviceRes.json());
|
|
20
|
+
// 2. Display instructions
|
|
21
|
+
const url = device.verificationUriComplete ?? device.verificationUri;
|
|
22
|
+
process.stderr.write(`\nVisit ${url}\n`);
|
|
23
|
+
process.stderr.write(`and enter code: ${device.userCode}\n\n`);
|
|
24
|
+
// 3. Try to open browser
|
|
25
|
+
this.openBrowser(url);
|
|
26
|
+
// 4. Poll for token
|
|
27
|
+
const tokens = await this.pollForToken(device);
|
|
28
|
+
// 5. Fetch user info
|
|
29
|
+
const user = await this.fetchUserInfo(tokens.accessToken);
|
|
30
|
+
// 6. Save credentials
|
|
31
|
+
const credentials = {
|
|
32
|
+
tokens,
|
|
33
|
+
user,
|
|
34
|
+
serverUrl: this.serverUrl,
|
|
35
|
+
};
|
|
36
|
+
await this.store.save(credentials);
|
|
37
|
+
return { user };
|
|
38
|
+
}
|
|
39
|
+
openBrowser(url) {
|
|
40
|
+
const cmd = platform() === 'darwin' ? 'open' : platform() === 'win32' ? 'cmd' : 'xdg-open';
|
|
41
|
+
const cmdArgs = platform() === 'win32' ? ['/c', 'start', '', url] : [url];
|
|
42
|
+
execFile(cmd, cmdArgs, () => {
|
|
43
|
+
// Ignore errors — user can open manually
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
async pollForToken(device) {
|
|
47
|
+
const deadline = Date.now() + device.expiresIn * 1000;
|
|
48
|
+
const interval = Math.max(device.interval, 5) * 1000;
|
|
49
|
+
while (Date.now() < deadline) {
|
|
50
|
+
await sleep(interval);
|
|
51
|
+
const res = await fetch(`${this.serverUrl}/oauth/token`, {
|
|
52
|
+
method: 'POST',
|
|
53
|
+
headers: { 'Content-Type': 'application/json' },
|
|
54
|
+
body: JSON.stringify({
|
|
55
|
+
grant_type: 'urn:ietf:params:oauth:grant-type:device_code',
|
|
56
|
+
device_code: device.deviceCode,
|
|
57
|
+
}),
|
|
58
|
+
});
|
|
59
|
+
if (res.ok) {
|
|
60
|
+
const data = (await res.json());
|
|
61
|
+
return {
|
|
62
|
+
accessToken: data.access_token,
|
|
63
|
+
refreshToken: data.refresh_token,
|
|
64
|
+
expiresAt: Date.now() + data.expires_in * 1000,
|
|
65
|
+
tokenType: data.token_type,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
const body = (await res.json());
|
|
69
|
+
if (body.error === 'authorization_pending') {
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
if (body.error === 'slow_down') {
|
|
73
|
+
await sleep(5000);
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
throw new Error(`Device code auth failed: ${body.error ?? res.statusText}`);
|
|
77
|
+
}
|
|
78
|
+
throw new Error('Device code expired. Please try again.');
|
|
79
|
+
}
|
|
80
|
+
async fetchUserInfo(accessToken) {
|
|
81
|
+
const res = await fetch(`${this.serverUrl}/oauth/userinfo`, {
|
|
82
|
+
headers: { Authorization: `Bearer ${accessToken}` },
|
|
83
|
+
});
|
|
84
|
+
if (!res.ok) {
|
|
85
|
+
throw new Error(`Failed to fetch user info (${res.status})`);
|
|
86
|
+
}
|
|
87
|
+
const data = (await res.json());
|
|
88
|
+
return {
|
|
89
|
+
id: data.id,
|
|
90
|
+
email: data.email,
|
|
91
|
+
tenantId: data.tenant_id,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
function sleep(ms) {
|
|
96
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
97
|
+
}
|
|
98
|
+
//# sourceMappingURL=device-code-auth.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"device-code-auth.js","sourceRoot":"","sources":["../src/device-code-auth.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { FileCredentialStore } from './credential-store.js';
|
|
2
|
+
export { DeviceCodeAuth } from './device-code-auth.js';
|
|
3
|
+
export { getValidToken, isTokenExpired, refreshTokens } from './token-refresh.js';
|
|
4
|
+
export type { ICredentialStore, StoredCredentials, TokenSet, UserInfo, DeviceCodeResponse, TokenResponse, UserInfoResponse, } from './types.js';
|
|
5
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAAE,MAAM,uBAAuB,CAAC;AAC5D,OAAO,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AACvD,OAAO,EAAE,aAAa,EAAE,cAAc,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AAClF,YAAY,EACX,gBAAgB,EAChB,iBAAiB,EACjB,QAAQ,EACR,QAAQ,EACR,kBAAkB,EAClB,aAAa,EACb,gBAAgB,GAChB,MAAM,YAAY,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAAE,MAAM,uBAAuB,CAAC;AAC5D,OAAO,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AACvD,OAAO,EAAE,aAAa,EAAE,cAAc,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { ICredentialStore, StoredCredentials, TokenSet } from './types.js';
|
|
2
|
+
export declare function isTokenExpired(tokens: TokenSet): boolean;
|
|
3
|
+
export declare function refreshTokens(credentials: StoredCredentials, store: ICredentialStore): Promise<TokenSet>;
|
|
4
|
+
/**
|
|
5
|
+
* Returns a valid access token, refreshing if expired.
|
|
6
|
+
*/
|
|
7
|
+
export declare function getValidToken(store: ICredentialStore): Promise<{
|
|
8
|
+
token: string;
|
|
9
|
+
credentials: StoredCredentials;
|
|
10
|
+
} | null>;
|
|
11
|
+
//# sourceMappingURL=token-refresh.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"token-refresh.d.ts","sourceRoot":"","sources":["../src/token-refresh.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,iBAAiB,EAAiB,QAAQ,EAAE,MAAM,YAAY,CAAC;AAI/F,wBAAgB,cAAc,CAAC,MAAM,EAAE,QAAQ,GAAG,OAAO,CAExD;AAED,wBAAsB,aAAa,CAClC,WAAW,EAAE,iBAAiB,EAC9B,KAAK,EAAE,gBAAgB,GACrB,OAAO,CAAC,QAAQ,CAAC,CA4BnB;AAED;;GAEG;AACH,wBAAsB,aAAa,CAClC,KAAK,EAAE,gBAAgB,GACrB,OAAO,CAAC;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,WAAW,EAAE,iBAAiB,CAAA;CAAE,GAAG,IAAI,CAAC,CAWnE"}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
const EXPIRY_BUFFER_MS = 60_000; // Refresh 1 minute before expiry
|
|
2
|
+
export function isTokenExpired(tokens) {
|
|
3
|
+
return Date.now() >= tokens.expiresAt - EXPIRY_BUFFER_MS;
|
|
4
|
+
}
|
|
5
|
+
export async function refreshTokens(credentials, store) {
|
|
6
|
+
if (!credentials.tokens.refreshToken) {
|
|
7
|
+
throw new Error('No refresh token available. Please login again.');
|
|
8
|
+
}
|
|
9
|
+
const response = await fetch(`${credentials.serverUrl}/oauth/token`, {
|
|
10
|
+
method: 'POST',
|
|
11
|
+
headers: { 'Content-Type': 'application/json' },
|
|
12
|
+
body: JSON.stringify({
|
|
13
|
+
grant_type: 'refresh_token',
|
|
14
|
+
refresh_token: credentials.tokens.refreshToken,
|
|
15
|
+
}),
|
|
16
|
+
});
|
|
17
|
+
if (!response.ok) {
|
|
18
|
+
throw new Error(`Token refresh failed (${response.status}). Please login again.`);
|
|
19
|
+
}
|
|
20
|
+
const data = (await response.json());
|
|
21
|
+
const newTokens = {
|
|
22
|
+
accessToken: data.access_token,
|
|
23
|
+
refreshToken: data.refresh_token ?? credentials.tokens.refreshToken,
|
|
24
|
+
expiresAt: Date.now() + data.expires_in * 1000,
|
|
25
|
+
tokenType: data.token_type,
|
|
26
|
+
};
|
|
27
|
+
await store.save({ ...credentials, tokens: newTokens });
|
|
28
|
+
return newTokens;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Returns a valid access token, refreshing if expired.
|
|
32
|
+
*/
|
|
33
|
+
export async function getValidToken(store) {
|
|
34
|
+
const credentials = await store.load();
|
|
35
|
+
if (!credentials)
|
|
36
|
+
return null;
|
|
37
|
+
if (!isTokenExpired(credentials.tokens)) {
|
|
38
|
+
return { token: credentials.tokens.accessToken, credentials };
|
|
39
|
+
}
|
|
40
|
+
const newTokens = await refreshTokens(credentials, store);
|
|
41
|
+
const updated = { ...credentials, tokens: newTokens };
|
|
42
|
+
return { token: updated.tokens.accessToken, credentials: updated };
|
|
43
|
+
}
|
|
44
|
+
//# sourceMappingURL=token-refresh.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"token-refresh.js","sourceRoot":"","sources":["../src/token-refresh.ts"],"names":[],"mappings":"AAEA,MAAM,gBAAgB,GAAG,MAAM,CAAC,CAAC,iCAAiC;AAElE,MAAM,UAAU,cAAc,CAAC,MAAgB;IAC9C,OAAO,IAAI,CAAC,GAAG,EAAE,IAAI,MAAM,CAAC,SAAS,GAAG,gBAAgB,CAAC;AAC1D,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,aAAa,CAClC,WAA8B,EAC9B,KAAuB;IAEvB,IAAI,CAAC,WAAW,CAAC,MAAM,CAAC,YAAY,EAAE,CAAC;QACtC,MAAM,IAAI,KAAK,CAAC,iDAAiD,CAAC,CAAC;IACpE,CAAC;IAED,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,GAAG,WAAW,CAAC,SAAS,cAAc,EAAE;QACpE,MAAM,EAAE,MAAM;QACd,OAAO,EAAE,EAAE,cAAc,EAAE,kBAAkB,EAAE;QAC/C,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC;YACpB,UAAU,EAAE,eAAe;YAC3B,aAAa,EAAE,WAAW,CAAC,MAAM,CAAC,YAAY;SAC9C,CAAC;KACF,CAAC,CAAC;IAEH,IAAI,CAAC,QAAQ,CAAC,EAAE,EAAE,CAAC;QAClB,MAAM,IAAI,KAAK,CAAC,yBAAyB,QAAQ,CAAC,MAAM,wBAAwB,CAAC,CAAC;IACnF,CAAC;IAED,MAAM,IAAI,GAAG,CAAC,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAkB,CAAC;IACtD,MAAM,SAAS,GAAa;QAC3B,WAAW,EAAE,IAAI,CAAC,YAAY;QAC9B,YAAY,EAAE,IAAI,CAAC,aAAa,IAAI,WAAW,CAAC,MAAM,CAAC,YAAY;QACnE,SAAS,EAAE,IAAI,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC,UAAU,GAAG,IAAI;QAC9C,SAAS,EAAE,IAAI,CAAC,UAAU;KAC1B,CAAC;IAEF,MAAM,KAAK,CAAC,IAAI,CAAC,EAAE,GAAG,WAAW,EAAE,MAAM,EAAE,SAAS,EAAE,CAAC,CAAC;IACxD,OAAO,SAAS,CAAC;AAClB,CAAC;AAED;;GAEG;AACH,MAAM,CAAC,KAAK,UAAU,aAAa,CAClC,KAAuB;IAEvB,MAAM,WAAW,GAAG,MAAM,KAAK,CAAC,IAAI,EAAE,CAAC;IACvC,IAAI,CAAC,WAAW;QAAE,OAAO,IAAI,CAAC;IAE9B,IAAI,CAAC,cAAc,CAAC,WAAW,CAAC,MAAM,CAAC,EAAE,CAAC;QACzC,OAAO,EAAE,KAAK,EAAE,WAAW,CAAC,MAAM,CAAC,WAAW,EAAE,WAAW,EAAE,CAAC;IAC/D,CAAC;IAED,MAAM,SAAS,GAAG,MAAM,aAAa,CAAC,WAAW,EAAE,KAAK,CAAC,CAAC;IAC1D,MAAM,OAAO,GAAG,EAAE,GAAG,WAAW,EAAE,MAAM,EAAE,SAAS,EAAE,CAAC;IACtD,OAAO,EAAE,KAAK,EAAE,OAAO,CAAC,MAAM,CAAC,WAAW,EAAE,WAAW,EAAE,OAAO,EAAE,CAAC;AACpE,CAAC"}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/** OAuth token set */
|
|
2
|
+
export interface TokenSet {
|
|
3
|
+
accessToken: string;
|
|
4
|
+
refreshToken?: string;
|
|
5
|
+
expiresAt: number;
|
|
6
|
+
tokenType: string;
|
|
7
|
+
}
|
|
8
|
+
/** User info from the auth server */
|
|
9
|
+
export interface UserInfo {
|
|
10
|
+
id: string;
|
|
11
|
+
email: string;
|
|
12
|
+
tenantId: string;
|
|
13
|
+
}
|
|
14
|
+
/** Stored credentials (persisted to disk) */
|
|
15
|
+
export interface StoredCredentials {
|
|
16
|
+
tokens: TokenSet;
|
|
17
|
+
user: UserInfo;
|
|
18
|
+
serverUrl: string;
|
|
19
|
+
}
|
|
20
|
+
/** Credential storage interface */
|
|
21
|
+
export interface ICredentialStore {
|
|
22
|
+
load(): Promise<StoredCredentials | null>;
|
|
23
|
+
save(credentials: StoredCredentials): Promise<void>;
|
|
24
|
+
clear(): Promise<void>;
|
|
25
|
+
}
|
|
26
|
+
/** Device code response from auth server */
|
|
27
|
+
export interface DeviceCodeResponse {
|
|
28
|
+
deviceCode: string;
|
|
29
|
+
userCode: string;
|
|
30
|
+
verificationUri: string;
|
|
31
|
+
verificationUriComplete?: string;
|
|
32
|
+
expiresIn: number;
|
|
33
|
+
interval: number;
|
|
34
|
+
}
|
|
35
|
+
/** Token response from auth server */
|
|
36
|
+
export interface TokenResponse {
|
|
37
|
+
access_token: string;
|
|
38
|
+
refresh_token?: string;
|
|
39
|
+
expires_in: number;
|
|
40
|
+
token_type: string;
|
|
41
|
+
}
|
|
42
|
+
/** User info response from auth server */
|
|
43
|
+
export interface UserInfoResponse {
|
|
44
|
+
id: string;
|
|
45
|
+
email: string;
|
|
46
|
+
tenant_id: string;
|
|
47
|
+
}
|
|
48
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,sBAAsB;AACtB,MAAM,WAAW,QAAQ;IACxB,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;CAClB;AAED,qCAAqC;AACrC,MAAM,WAAW,QAAQ;IACxB,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;CACjB;AAED,6CAA6C;AAC7C,MAAM,WAAW,iBAAiB;IACjC,MAAM,EAAE,QAAQ,CAAC;IACjB,IAAI,EAAE,QAAQ,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;CAClB;AAED,mCAAmC;AACnC,MAAM,WAAW,gBAAgB;IAChC,IAAI,IAAI,OAAO,CAAC,iBAAiB,GAAG,IAAI,CAAC,CAAC;IAC1C,IAAI,CAAC,WAAW,EAAE,iBAAiB,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACpD,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACvB;AAED,4CAA4C;AAC5C,MAAM,WAAW,kBAAkB;IAClC,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,eAAe,EAAE,MAAM,CAAC;IACxB,uBAAuB,CAAC,EAAE,MAAM,CAAC;IACjC,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;CACjB;AAED,sCAAsC;AACtC,MAAM,WAAW,aAAa;IAC7B,YAAY,EAAE,MAAM,CAAC;IACrB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;CACnB;AAED,0CAA0C;AAC1C,MAAM,WAAW,gBAAgB;IAChC,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,MAAM,CAAC;CAClB"}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":""}
|
package/package.json
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "viyv-db-auth",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "Authentication implementation for viyv-db",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"author": "viyv",
|
|
8
|
+
"engines": {
|
|
9
|
+
"node": ">=20"
|
|
10
|
+
},
|
|
11
|
+
"main": "dist/index.js",
|
|
12
|
+
"types": "dist/index.d.ts",
|
|
13
|
+
"exports": {
|
|
14
|
+
".": {
|
|
15
|
+
"types": "./dist/index.d.ts",
|
|
16
|
+
"default": "./dist/index.js"
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
"files": [
|
|
20
|
+
"dist",
|
|
21
|
+
"!dist/__tests__"
|
|
22
|
+
],
|
|
23
|
+
"dependencies": {
|
|
24
|
+
"viyv-db-core": "0.1.0"
|
|
25
|
+
},
|
|
26
|
+
"devDependencies": {
|
|
27
|
+
"typescript": "^5.7.0",
|
|
28
|
+
"vitest": "^2.1.0"
|
|
29
|
+
},
|
|
30
|
+
"scripts": {
|
|
31
|
+
"build": "tsc",
|
|
32
|
+
"typecheck": "tsc --noEmit",
|
|
33
|
+
"test": "vitest run",
|
|
34
|
+
"clean": "rm -rf dist"
|
|
35
|
+
}
|
|
36
|
+
}
|