yoto-nodejs-client 0.0.1
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/README.md +736 -0
- package/bin/auth.d.ts +3 -0
- package/bin/auth.d.ts.map +1 -0
- package/bin/auth.js +130 -0
- package/bin/content.d.ts +3 -0
- package/bin/content.d.ts.map +1 -0
- package/bin/content.js +117 -0
- package/bin/devices.d.ts +3 -0
- package/bin/devices.d.ts.map +1 -0
- package/bin/devices.js +239 -0
- package/bin/groups.d.ts +3 -0
- package/bin/groups.d.ts.map +1 -0
- package/bin/groups.js +80 -0
- package/bin/icons.d.ts +3 -0
- package/bin/icons.d.ts.map +1 -0
- package/bin/icons.js +100 -0
- package/bin/lib/cli-helpers.d.ts +21 -0
- package/bin/lib/cli-helpers.d.ts.map +1 -0
- package/bin/lib/cli-helpers.js +140 -0
- package/bin/lib/token-helpers.d.ts +14 -0
- package/bin/lib/token-helpers.d.ts.map +1 -0
- package/bin/lib/token-helpers.js +151 -0
- package/bin/refresh-token.d.ts +3 -0
- package/bin/refresh-token.d.ts.map +1 -0
- package/bin/refresh-token.js +168 -0
- package/bin/token-info.d.ts +3 -0
- package/bin/token-info.d.ts.map +1 -0
- package/bin/token-info.js +351 -0
- package/index.d.ts +218 -0
- package/index.d.ts.map +1 -0
- package/index.js +689 -0
- package/lib/api-endpoints/auth.d.ts +56 -0
- package/lib/api-endpoints/auth.d.ts.map +1 -0
- package/lib/api-endpoints/auth.js +209 -0
- package/lib/api-endpoints/auth.test.js +27 -0
- package/lib/api-endpoints/constants.d.ts +6 -0
- package/lib/api-endpoints/constants.d.ts.map +1 -0
- package/lib/api-endpoints/constants.js +31 -0
- package/lib/api-endpoints/content.d.ts +275 -0
- package/lib/api-endpoints/content.d.ts.map +1 -0
- package/lib/api-endpoints/content.js +518 -0
- package/lib/api-endpoints/content.test.js +250 -0
- package/lib/api-endpoints/devices.d.ts +202 -0
- package/lib/api-endpoints/devices.d.ts.map +1 -0
- package/lib/api-endpoints/devices.js +404 -0
- package/lib/api-endpoints/devices.test.js +483 -0
- package/lib/api-endpoints/family-library-groups.d.ts +75 -0
- package/lib/api-endpoints/family-library-groups.d.ts.map +1 -0
- package/lib/api-endpoints/family-library-groups.js +247 -0
- package/lib/api-endpoints/family-library-groups.test.js +272 -0
- package/lib/api-endpoints/family.d.ts +39 -0
- package/lib/api-endpoints/family.d.ts.map +1 -0
- package/lib/api-endpoints/family.js +166 -0
- package/lib/api-endpoints/family.test.js +184 -0
- package/lib/api-endpoints/helpers.d.ts +29 -0
- package/lib/api-endpoints/helpers.d.ts.map +1 -0
- package/lib/api-endpoints/helpers.js +104 -0
- package/lib/api-endpoints/icons.d.ts +62 -0
- package/lib/api-endpoints/icons.d.ts.map +1 -0
- package/lib/api-endpoints/icons.js +201 -0
- package/lib/api-endpoints/icons.test.js +118 -0
- package/lib/api-endpoints/media.d.ts +37 -0
- package/lib/api-endpoints/media.d.ts.map +1 -0
- package/lib/api-endpoints/media.js +155 -0
- package/lib/api-endpoints/test-helpers.d.ts +7 -0
- package/lib/api-endpoints/test-helpers.d.ts.map +1 -0
- package/lib/api-endpoints/test-helpers.js +64 -0
- package/lib/mqtt/client.d.ts +124 -0
- package/lib/mqtt/client.d.ts.map +1 -0
- package/lib/mqtt/client.js +558 -0
- package/lib/mqtt/commands.d.ts +69 -0
- package/lib/mqtt/commands.d.ts.map +1 -0
- package/lib/mqtt/commands.js +238 -0
- package/lib/mqtt/factory.d.ts +12 -0
- package/lib/mqtt/factory.d.ts.map +1 -0
- package/lib/mqtt/factory.js +107 -0
- package/lib/mqtt/index.d.ts +5 -0
- package/lib/mqtt/index.d.ts.map +1 -0
- package/lib/mqtt/index.js +81 -0
- package/lib/mqtt/mqtt.test.js +168 -0
- package/lib/mqtt/topics.d.ts +34 -0
- package/lib/mqtt/topics.d.ts.map +1 -0
- package/lib/mqtt/topics.js +295 -0
- package/lib/pkg.cjs +3 -0
- package/lib/pkg.d.cts +70 -0
- package/lib/pkg.d.cts.map +1 -0
- package/lib/token.d.ts +29 -0
- package/lib/token.d.ts.map +1 -0
- package/lib/token.js +240 -0
- package/package.json +91 -0
- package/yoto.png +0 -0
package/bin/icons.js
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @import {ArgscloptsParseArgsOptionsConfig} from 'argsclopts'
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { printHelpText } from 'argsclopts'
|
|
8
|
+
import { parseArgs } from 'node:util'
|
|
9
|
+
import { pkg } from '../lib/pkg.cjs'
|
|
10
|
+
import {
|
|
11
|
+
getCommonOptions,
|
|
12
|
+
loadTokensFromEnv,
|
|
13
|
+
createYotoClient,
|
|
14
|
+
handleCliError,
|
|
15
|
+
printHeader
|
|
16
|
+
} from './lib/cli-helpers.js'
|
|
17
|
+
|
|
18
|
+
/** @type {ArgscloptsParseArgsOptionsConfig} */
|
|
19
|
+
const options = {
|
|
20
|
+
...getCommonOptions(),
|
|
21
|
+
public: {
|
|
22
|
+
type: 'boolean',
|
|
23
|
+
short: 'p',
|
|
24
|
+
help: 'Show only public Yoto icons'
|
|
25
|
+
},
|
|
26
|
+
user: {
|
|
27
|
+
type: 'boolean',
|
|
28
|
+
short: 'u',
|
|
29
|
+
help: 'Show only user custom icons'
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const args = parseArgs({ options, strict: false })
|
|
34
|
+
|
|
35
|
+
if (args.values['help']) {
|
|
36
|
+
await printHelpText({
|
|
37
|
+
options,
|
|
38
|
+
name: 'yoto-icons',
|
|
39
|
+
version: pkg.version,
|
|
40
|
+
exampleFn: ({ name }) => ` Yoto icons information helper\n\n Examples:\n ${name} # List both public and user icons\n ${name} --public # List only public Yoto icons\n ${name} --user # List only user custom icons\n`
|
|
41
|
+
})
|
|
42
|
+
process.exit(0)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Load tokens from environment
|
|
46
|
+
const { clientId, refreshToken, accessToken, envFile } = loadTokensFromEnv(args)
|
|
47
|
+
|
|
48
|
+
const showPublic = Boolean(args.values['public'])
|
|
49
|
+
const showUser = Boolean(args.values['user'])
|
|
50
|
+
|
|
51
|
+
// If neither flag is set, show both
|
|
52
|
+
const showBoth = !showPublic && !showUser
|
|
53
|
+
|
|
54
|
+
async function main () {
|
|
55
|
+
printHeader('Yoto Icons')
|
|
56
|
+
|
|
57
|
+
try {
|
|
58
|
+
// Create client
|
|
59
|
+
const client = createYotoClient({
|
|
60
|
+
clientId,
|
|
61
|
+
refreshToken,
|
|
62
|
+
accessToken,
|
|
63
|
+
outputFile: envFile
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
if (showPublic || showBoth) {
|
|
67
|
+
console.log('\nFetching public Yoto icons...\n')
|
|
68
|
+
const publicIcons = await client.getPublicIcons()
|
|
69
|
+
|
|
70
|
+
if (showBoth) {
|
|
71
|
+
console.log('Public Icons:')
|
|
72
|
+
}
|
|
73
|
+
console.dir(publicIcons, { depth: null, colors: true })
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (showUser || showBoth) {
|
|
77
|
+
if (showBoth) {
|
|
78
|
+
console.log('\n' + '='.repeat(60) + '\n')
|
|
79
|
+
}
|
|
80
|
+
console.log('\nFetching user custom icons...\n')
|
|
81
|
+
const userIcons = await client.getUserIcons()
|
|
82
|
+
|
|
83
|
+
if (showBoth) {
|
|
84
|
+
console.log('User Icons:')
|
|
85
|
+
}
|
|
86
|
+
console.dir(userIcons, { depth: null, colors: true })
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
process.exit(0)
|
|
90
|
+
} catch (error) {
|
|
91
|
+
handleCliError(error)
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Run the main function
|
|
96
|
+
await main().catch(err => {
|
|
97
|
+
console.error('\n❌ Unexpected error:')
|
|
98
|
+
console.error(err)
|
|
99
|
+
process.exit(1)
|
|
100
|
+
})
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export function getCommonOptions(): ArgscloptsParseArgsOptionsConfig;
|
|
2
|
+
export function loadEnvFile(envFile?: string): string;
|
|
3
|
+
export function loadTokensFromEnv(args: {
|
|
4
|
+
values: Record<string, string | boolean | undefined>;
|
|
5
|
+
}): {
|
|
6
|
+
clientId: string;
|
|
7
|
+
refreshToken: string;
|
|
8
|
+
accessToken: string;
|
|
9
|
+
envFile: string;
|
|
10
|
+
};
|
|
11
|
+
export function createYotoClient({ clientId, refreshToken, accessToken, outputFile }: {
|
|
12
|
+
clientId: string;
|
|
13
|
+
refreshToken: string;
|
|
14
|
+
accessToken: string;
|
|
15
|
+
outputFile?: string | undefined;
|
|
16
|
+
}): YotoClient;
|
|
17
|
+
export function handleCliError(error: any): void;
|
|
18
|
+
export function printHeader(title: string): void;
|
|
19
|
+
import type { ArgscloptsParseArgsOptionsConfig } from 'argsclopts';
|
|
20
|
+
import { YotoClient } from '../../index.js';
|
|
21
|
+
//# sourceMappingURL=cli-helpers.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli-helpers.d.ts","sourceRoot":"","sources":["cli-helpers.js"],"names":[],"mappings":"AAYA,oCAFa,gCAAgC,CAoB5C;AAOD,sCAHW,MAAM,GACJ,MAAM,CAUlB;AAQD,wCAJW;IAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,GAAG,SAAS,CAAC,CAAA;CAAE,GACtD;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,YAAY,EAAE,MAAM,CAAC;IAAC,WAAW,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAoB5F;AAWD,sFANG;IAAwB,QAAQ,EAAxB,MAAM;IACU,YAAY,EAA5B,MAAM;IACU,WAAW,EAA3B,MAAM;IACW,UAAU;CACnC,GAAU,UAAU,CAsBtB;AAMD,sCAFW,GAAG,QAyBb;AAMD,mCAFW,MAAM,QAKhB;sDA1IoD,YAAY;2BAGtC,gBAAgB"}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @import { ArgscloptsParseArgsOptionsConfig } from 'argsclopts'
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { YotoClient } from '../../index.js'
|
|
6
|
+
import { DEFAULT_CLIENT_ID } from '../../lib/api-endpoints/constants.js'
|
|
7
|
+
import { saveTokensToEnv } from './token-helpers.js'
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Get common CLI option definitions shared across all tools
|
|
11
|
+
* @returns {ArgscloptsParseArgsOptionsConfig}
|
|
12
|
+
*/
|
|
13
|
+
export function getCommonOptions () {
|
|
14
|
+
return {
|
|
15
|
+
help: {
|
|
16
|
+
type: 'boolean',
|
|
17
|
+
short: 'h',
|
|
18
|
+
help: 'Print help text'
|
|
19
|
+
},
|
|
20
|
+
'client-id': {
|
|
21
|
+
type: 'string',
|
|
22
|
+
short: 'c',
|
|
23
|
+
help: 'Yoto OAuth client ID (or set YOTO_CLIENT_ID env var)'
|
|
24
|
+
},
|
|
25
|
+
'env-file': {
|
|
26
|
+
type: 'string',
|
|
27
|
+
short: 'e',
|
|
28
|
+
help: 'Path to .env file to load (default: .env in current directory)'
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Load .env file from specified path or default to .env in cwd
|
|
35
|
+
* @param {string} [envFile] - Optional path to .env file
|
|
36
|
+
* @returns {string} The path that was loaded (or attempted)
|
|
37
|
+
*/
|
|
38
|
+
export function loadEnvFile (envFile) {
|
|
39
|
+
const envPath = envFile || '.env'
|
|
40
|
+
try {
|
|
41
|
+
process.loadEnvFile(envPath)
|
|
42
|
+
} catch (err) {
|
|
43
|
+
// File doesn't exist or can't be loaded, that's okay
|
|
44
|
+
}
|
|
45
|
+
return envPath
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Load and validate tokens from environment
|
|
50
|
+
* @param {{ values: Record<string, string | boolean | undefined> }} args - Parsed arguments from parseArgs
|
|
51
|
+
* @returns {{ clientId: string, refreshToken: string, accessToken: string, envFile: string }}
|
|
52
|
+
* @throws Exits process if tokens are missing
|
|
53
|
+
*/
|
|
54
|
+
export function loadTokensFromEnv (args) {
|
|
55
|
+
const envFile = loadEnvFile(args.values['env-file'] ? String(args.values['env-file']) : undefined)
|
|
56
|
+
|
|
57
|
+
const clientId = String(args.values['client-id'] || process.env['YOTO_CLIENT_ID'] || DEFAULT_CLIENT_ID)
|
|
58
|
+
const refreshToken = String(process.env['YOTO_REFRESH_TOKEN'] || '')
|
|
59
|
+
const accessToken = String(process.env['YOTO_ACCESS_TOKEN'] || '')
|
|
60
|
+
|
|
61
|
+
if (!accessToken || !refreshToken) {
|
|
62
|
+
console.error('❌ Both access token and refresh token are required')
|
|
63
|
+
console.error('Provide tokens via:')
|
|
64
|
+
console.error(' - Environment variables: YOTO_ACCESS_TOKEN and YOTO_REFRESH_TOKEN')
|
|
65
|
+
console.error(' - .env file (default or specify with --env-file)')
|
|
66
|
+
console.error('\n💡 Tip: Run yoto-auth to get both tokens')
|
|
67
|
+
process.exit(1)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return { clientId, refreshToken, accessToken, envFile }
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Create YotoClient with standard token persistence callbacks
|
|
75
|
+
* @param {object} options
|
|
76
|
+
* @param {string} options.clientId - OAuth client ID
|
|
77
|
+
* @param {string} options.refreshToken - OAuth refresh token
|
|
78
|
+
* @param {string} options.accessToken - OAuth access token
|
|
79
|
+
* @param {string} [options.outputFile='.env'] - File to save refreshed tokens to
|
|
80
|
+
* @returns {YotoClient}
|
|
81
|
+
*/
|
|
82
|
+
export function createYotoClient ({ clientId, refreshToken, accessToken, outputFile = '.env' }) {
|
|
83
|
+
return new YotoClient({
|
|
84
|
+
clientId,
|
|
85
|
+
refreshToken,
|
|
86
|
+
accessToken,
|
|
87
|
+
onTokenRefresh: async (tokens) => {
|
|
88
|
+
// Save tokens if they refresh during operation
|
|
89
|
+
await saveTokensToEnv(outputFile, {
|
|
90
|
+
access_token: tokens.accessToken,
|
|
91
|
+
refresh_token: tokens.refreshToken,
|
|
92
|
+
token_type: 'Bearer',
|
|
93
|
+
expires_in: tokens.expiresAt - Math.floor(Date.now() / 1000)
|
|
94
|
+
}, tokens.clientId)
|
|
95
|
+
},
|
|
96
|
+
onRefreshStart: () => {
|
|
97
|
+
console.log('\n🔄 Token refresh triggered...')
|
|
98
|
+
},
|
|
99
|
+
onRefreshError: () => {},
|
|
100
|
+
onInvalid: () => {}
|
|
101
|
+
})
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Standard error handler for CLI tools
|
|
106
|
+
* @param {any} error
|
|
107
|
+
*/
|
|
108
|
+
export function handleCliError (error) {
|
|
109
|
+
console.error('\n❌ Error:')
|
|
110
|
+
console.error(`Message: ${error.message}`)
|
|
111
|
+
|
|
112
|
+
// Show detailed error information if available
|
|
113
|
+
if (error.statusCode) {
|
|
114
|
+
console.error(`Status Code: ${error.statusCode}`)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (error.body?.error) {
|
|
118
|
+
console.error(`Error: ${error.body.error}`)
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (error.body?.error_description) {
|
|
122
|
+
console.error(`Description: ${error.body.error_description}`)
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (error.body && typeof error.body === 'object') {
|
|
126
|
+
console.error('\nFull error response:')
|
|
127
|
+
console.error(JSON.stringify(error.body, null, 2))
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
process.exit(1)
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Print CLI tool header with title
|
|
135
|
+
* @param {string} title - The title to display
|
|
136
|
+
*/
|
|
137
|
+
export function printHeader (title) {
|
|
138
|
+
console.log(`🎵 ${title}`)
|
|
139
|
+
console.log('='.repeat(60))
|
|
140
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export function formatTimestamp(timestamp: number): string;
|
|
2
|
+
export function checkExpiration(exp: number, bufferSeconds?: number): {
|
|
3
|
+
expired: boolean;
|
|
4
|
+
message: string;
|
|
5
|
+
};
|
|
6
|
+
export function checkTokenExpiration(token: string, bufferSeconds?: number): {
|
|
7
|
+
expired: boolean;
|
|
8
|
+
message: string;
|
|
9
|
+
};
|
|
10
|
+
export function decodeJwt(token: string): any;
|
|
11
|
+
export function saveTokensToEnv(filename: string, tokens: YotoTokenResponse, clientId: string): Promise<void>;
|
|
12
|
+
export function sleep(ms: number): Promise<any>;
|
|
13
|
+
import type { YotoTokenResponse } from '../../lib/api-endpoints/auth.js';
|
|
14
|
+
//# sourceMappingURL=token-helpers.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"token-helpers.d.ts","sourceRoot":"","sources":["token-helpers.js"],"names":[],"mappings":"AAaA,2CAHW,MAAM,GACJ,MAAM,CAKlB;AAQD,qCAJW,MAAM,kBACN,MAAM,GACJ;IAAC,OAAO,EAAE,OAAO,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAC,CAmB/C;AAQD,4CAJW,MAAM,kBACN,MAAM,GACJ;IAAC,OAAO,EAAE,OAAO,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAC,CAa/C;AAOD,iCAHW,MAAM,GACJ,GAAG,CASf;AAQD,0CAJW,MAAM,UACN,iBAAiB,YACjB,MAAM,iBA8DhB;AAMD,0BAFW,MAAM,gBAIhB;uCArJmC,iCAAiC"}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @import {YotoTokenResponse} from '../../lib/api-endpoints/auth.js'
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { promises as fs } from 'node:fs'
|
|
6
|
+
import { join } from 'node:path'
|
|
7
|
+
import { jwtDecode } from 'jwt-decode'
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Format timestamp
|
|
11
|
+
* @param {number} timestamp
|
|
12
|
+
* @returns {string}
|
|
13
|
+
*/
|
|
14
|
+
export function formatTimestamp (timestamp) {
|
|
15
|
+
const date = new Date(timestamp * 1000)
|
|
16
|
+
return date.toISOString()
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Check if token is expired
|
|
21
|
+
* @param {number} exp
|
|
22
|
+
* @param {number} bufferSeconds
|
|
23
|
+
* @returns {{expired: boolean, message: string}}
|
|
24
|
+
*/
|
|
25
|
+
export function checkExpiration (exp, bufferSeconds = 30) {
|
|
26
|
+
const now = Math.floor(Date.now() / 1000)
|
|
27
|
+
const timeUntilExpiry = exp - now
|
|
28
|
+
|
|
29
|
+
if (timeUntilExpiry < 0) {
|
|
30
|
+
return { expired: true, message: `Expired ${Math.abs(timeUntilExpiry)} seconds ago` }
|
|
31
|
+
} else if (timeUntilExpiry < bufferSeconds) {
|
|
32
|
+
return { expired: true, message: `Expires in ${timeUntilExpiry} seconds (within buffer)` }
|
|
33
|
+
} else {
|
|
34
|
+
const minutes = Math.floor(timeUntilExpiry / 60)
|
|
35
|
+
const hours = Math.floor(minutes / 60)
|
|
36
|
+
if (hours > 0) {
|
|
37
|
+
return { expired: false, message: `Valid for ${hours} hour(s) ${minutes % 60} minute(s)` }
|
|
38
|
+
} else {
|
|
39
|
+
return { expired: false, message: `Valid for ${minutes} minute(s)` }
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Check if a JWT token is expired or about to expire
|
|
46
|
+
* @param {string} token - JWT token to check
|
|
47
|
+
* @param {number} bufferSeconds - Seconds before expiration to consider expired
|
|
48
|
+
* @returns {{expired: boolean, message: string}}
|
|
49
|
+
*/
|
|
50
|
+
export function checkTokenExpiration (token, bufferSeconds = 30) {
|
|
51
|
+
try {
|
|
52
|
+
const decoded = jwtDecode(token)
|
|
53
|
+
if (!decoded.exp) {
|
|
54
|
+
return { expired: true, message: 'Token has no expiration claim' }
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return checkExpiration(decoded.exp, bufferSeconds)
|
|
58
|
+
} catch (err) {
|
|
59
|
+
return { expired: true, message: 'Unable to decode token' }
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Decode a JWT token without verification
|
|
65
|
+
* @param {string} token
|
|
66
|
+
* @returns {any}
|
|
67
|
+
*/
|
|
68
|
+
export function decodeJwt (token) {
|
|
69
|
+
try {
|
|
70
|
+
return jwtDecode(token)
|
|
71
|
+
} catch (err) {
|
|
72
|
+
const error = /** @type {any} */ (err)
|
|
73
|
+
throw new Error(`Failed to decode JWT: ${error.message}`)
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Save tokens to .env file
|
|
79
|
+
* @param {string} filename
|
|
80
|
+
* @param {YotoTokenResponse} tokens
|
|
81
|
+
* @param {string} clientId
|
|
82
|
+
*/
|
|
83
|
+
export async function saveTokensToEnv (filename, tokens, clientId) {
|
|
84
|
+
const cwd = process.cwd()
|
|
85
|
+
const filePath = join(cwd, filename)
|
|
86
|
+
|
|
87
|
+
let existingContent = ''
|
|
88
|
+
|
|
89
|
+
// Read existing .env file if it exists
|
|
90
|
+
try {
|
|
91
|
+
existingContent = await fs.readFile(filePath, 'utf8')
|
|
92
|
+
} catch (err) {
|
|
93
|
+
// File doesn't exist, that's okay
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Parse existing content and remove old Yoto tokens (including comment lines)
|
|
97
|
+
const lines = existingContent.split('\n').filter(line => {
|
|
98
|
+
const trimmed = line.trim()
|
|
99
|
+
return !trimmed.startsWith('YOTO_ACCESS_TOKEN=') &&
|
|
100
|
+
!trimmed.startsWith('YOTO_REFRESH_TOKEN=') &&
|
|
101
|
+
!trimmed.startsWith('YOTO_CLIENT_ID=') &&
|
|
102
|
+
!trimmed.startsWith('# Yoto API tokens') &&
|
|
103
|
+
!trimmed.startsWith('# Saved at:') &&
|
|
104
|
+
!trimmed.startsWith('# Access token expires:')
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
// Remove trailing empty lines
|
|
108
|
+
while (lines.length > 0) {
|
|
109
|
+
const lastLine = lines[lines.length - 1]
|
|
110
|
+
if (lastLine && lastLine.trim() === '') {
|
|
111
|
+
lines.pop()
|
|
112
|
+
} else {
|
|
113
|
+
break
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Get token expiration for comment
|
|
118
|
+
let expirationComment = ''
|
|
119
|
+
try {
|
|
120
|
+
const decoded = decodeJwt(tokens.access_token)
|
|
121
|
+
if (decoded.exp) {
|
|
122
|
+
expirationComment = `# Access token expires: ${formatTimestamp(decoded.exp)}\n`
|
|
123
|
+
}
|
|
124
|
+
} catch (err) {
|
|
125
|
+
// If we can't decode, just skip the expiration comment
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const now = new Date().toISOString()
|
|
129
|
+
|
|
130
|
+
// Add new tokens (only add leading blank line if file has content)
|
|
131
|
+
if (lines.length > 0 && lines[lines.length - 1] !== '') {
|
|
132
|
+
lines.push('')
|
|
133
|
+
}
|
|
134
|
+
lines.push('# Yoto API tokens')
|
|
135
|
+
lines.push(`# Saved at: ${now}`)
|
|
136
|
+
if (expirationComment) lines.push(expirationComment.trim())
|
|
137
|
+
lines.push(`YOTO_ACCESS_TOKEN=${tokens.access_token}`)
|
|
138
|
+
lines.push(`YOTO_REFRESH_TOKEN=${tokens.refresh_token}`)
|
|
139
|
+
lines.push(`YOTO_CLIENT_ID=${clientId}`)
|
|
140
|
+
|
|
141
|
+
// Write back to file
|
|
142
|
+
await fs.writeFile(filePath, lines.join('\n'), 'utf8')
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Sleep for a specified number of milliseconds
|
|
147
|
+
* @param {number} ms
|
|
148
|
+
*/
|
|
149
|
+
export async function sleep (ms) {
|
|
150
|
+
return new Promise(resolve => setTimeout(resolve, ms))
|
|
151
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"refresh-token.d.ts","sourceRoot":"","sources":["refresh-token.js"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @import { ArgscloptsParseArgsOptionsConfig } from 'argsclopts'
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { printHelpText } from 'argsclopts'
|
|
8
|
+
import { parseArgs } from 'node:util'
|
|
9
|
+
import { YotoClient } from '../index.js'
|
|
10
|
+
import { pkg } from '../lib/pkg.cjs'
|
|
11
|
+
import { saveTokensToEnv } from './lib/token-helpers.js'
|
|
12
|
+
import {
|
|
13
|
+
getCommonOptions,
|
|
14
|
+
loadEnvFile,
|
|
15
|
+
printHeader,
|
|
16
|
+
handleCliError
|
|
17
|
+
} from './lib/cli-helpers.js'
|
|
18
|
+
import { DEFAULT_CLIENT_ID } from '../lib/api-endpoints/constants.js'
|
|
19
|
+
|
|
20
|
+
/** @type {ArgscloptsParseArgsOptionsConfig} */
|
|
21
|
+
const options = {
|
|
22
|
+
...getCommonOptions(),
|
|
23
|
+
'refresh-token': {
|
|
24
|
+
type: 'string',
|
|
25
|
+
short: 'r',
|
|
26
|
+
help: 'Refresh token to use (or set YOTO_REFRESH_TOKEN env var)'
|
|
27
|
+
},
|
|
28
|
+
output: {
|
|
29
|
+
type: 'string',
|
|
30
|
+
short: 'o',
|
|
31
|
+
default: '.env',
|
|
32
|
+
help: 'Output file for tokens (default: .env)'
|
|
33
|
+
},
|
|
34
|
+
force: {
|
|
35
|
+
type: 'boolean',
|
|
36
|
+
short: 'f',
|
|
37
|
+
help: 'Force refresh even if token is not expired'
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const args = parseArgs({ options, strict: false })
|
|
42
|
+
|
|
43
|
+
if (args.values['help']) {
|
|
44
|
+
await printHelpText({
|
|
45
|
+
options,
|
|
46
|
+
name: 'yoto-refresh-token',
|
|
47
|
+
version: pkg.version,
|
|
48
|
+
exampleFn: ({ name }) => ` Yoto token refresh helper\n\n Examples:\n ${name}\n ${name} --force\n ${name} --env-file .env.local\n`
|
|
49
|
+
})
|
|
50
|
+
process.exit(0)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Load .env file if specified or use default
|
|
54
|
+
const outputFile = String(args.values['output'] || '.env')
|
|
55
|
+
loadEnvFile(args.values['env-file'] ? String(args.values['env-file']) : outputFile)
|
|
56
|
+
|
|
57
|
+
const clientId = String(args.values['client-id'] || process.env['YOTO_CLIENT_ID'] || DEFAULT_CLIENT_ID)
|
|
58
|
+
const refreshToken = String(args.values['refresh-token'] || process.env['YOTO_REFRESH_TOKEN'] || '')
|
|
59
|
+
const accessToken = String(process.env['YOTO_ACCESS_TOKEN'] || '')
|
|
60
|
+
const force = Boolean(args.values['force'])
|
|
61
|
+
|
|
62
|
+
if (!refreshToken) {
|
|
63
|
+
console.error('❌ No refresh token found')
|
|
64
|
+
console.error('Provide a refresh token via:')
|
|
65
|
+
console.error(' - Command line flag: --refresh-token')
|
|
66
|
+
console.error(' - Environment variable: YOTO_REFRESH_TOKEN')
|
|
67
|
+
console.error(' - .env file (loaded automatically or specify with --env-file)')
|
|
68
|
+
console.error('\n💡 Tip: Run yoto-auth to get a refresh token')
|
|
69
|
+
process.exit(1)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (!accessToken) {
|
|
73
|
+
console.error('❌ No access token found')
|
|
74
|
+
console.error('Provide an access token via:')
|
|
75
|
+
console.error(' - Environment variable: YOTO_ACCESS_TOKEN')
|
|
76
|
+
console.error(' - .env file (loaded automatically or specify with --env-file)')
|
|
77
|
+
console.error('\n💡 Tip: Run yoto-auth to get an access token')
|
|
78
|
+
process.exit(1)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async function main () {
|
|
82
|
+
printHeader('Yoto Token Refresh')
|
|
83
|
+
|
|
84
|
+
try {
|
|
85
|
+
// Create client with token refresh handler
|
|
86
|
+
const client = new YotoClient({
|
|
87
|
+
clientId,
|
|
88
|
+
refreshToken,
|
|
89
|
+
accessToken,
|
|
90
|
+
onTokenRefresh: async (tokens) => {
|
|
91
|
+
// This will be called after successful refresh
|
|
92
|
+
// Convert RefreshSuccessEvent to YotoTokenResponse format
|
|
93
|
+
await saveTokensToEnv(outputFile, {
|
|
94
|
+
access_token: tokens.accessToken,
|
|
95
|
+
refresh_token: tokens.refreshToken,
|
|
96
|
+
token_type: 'Bearer',
|
|
97
|
+
expires_in: tokens.expiresAt - Math.floor(Date.now() / 1000)
|
|
98
|
+
}, tokens.clientId)
|
|
99
|
+
},
|
|
100
|
+
onRefreshStart: () => {
|
|
101
|
+
console.log('\n🔄 Refreshing tokens...')
|
|
102
|
+
},
|
|
103
|
+
onRefreshError: (error) => {
|
|
104
|
+
console.error('\n⚠️ Token refresh failed:', error.message)
|
|
105
|
+
},
|
|
106
|
+
onInvalid: (error) => {
|
|
107
|
+
console.error('\n❌ Refresh token is invalid or expired')
|
|
108
|
+
console.error(error.message)
|
|
109
|
+
console.error('\n💡 Tip: Run yoto-auth to get a new set of tokens.')
|
|
110
|
+
}
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
// Check if token needs refresh
|
|
114
|
+
if (!force && client.token.isValid()) {
|
|
115
|
+
const timeRemaining = client.token.getTimeRemaining()
|
|
116
|
+
const minutes = Math.floor(timeRemaining / 60)
|
|
117
|
+
const hours = Math.floor(minutes / 60)
|
|
118
|
+
if (hours > 0) {
|
|
119
|
+
console.log(`\n✅ Token is still valid for ${hours} hour(s) ${minutes % 60} minute(s)`)
|
|
120
|
+
} else {
|
|
121
|
+
console.log(`\n✅ Token is still valid for ${minutes} minute(s)`)
|
|
122
|
+
}
|
|
123
|
+
console.log('💡 Use --force to refresh anyway')
|
|
124
|
+
process.exit(0)
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (force) {
|
|
128
|
+
console.log('\n⚡ Forcing token refresh...')
|
|
129
|
+
} else {
|
|
130
|
+
console.log('\n⚠️ Token needs refresh')
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Manually trigger refresh
|
|
134
|
+
const tokens = await client.token.refresh()
|
|
135
|
+
|
|
136
|
+
console.log('\n✅ Token refresh successful!')
|
|
137
|
+
|
|
138
|
+
// Show expiration info
|
|
139
|
+
const expiresDate = new Date(tokens.expiresAt * 1000)
|
|
140
|
+
const timeRemaining = tokens.expiresAt - Math.floor(Date.now() / 1000)
|
|
141
|
+
const minutes = Math.floor(timeRemaining / 60)
|
|
142
|
+
const hours = Math.floor(minutes / 60)
|
|
143
|
+
|
|
144
|
+
if (hours > 0) {
|
|
145
|
+
console.log(`⏰ New token valid for ${hours} hour(s) ${minutes % 60} minute(s)`)
|
|
146
|
+
} else {
|
|
147
|
+
console.log(`⏰ New token valid for ${minutes} minute(s)`)
|
|
148
|
+
}
|
|
149
|
+
console.log(` Expires: ${expiresDate.toISOString()}`)
|
|
150
|
+
|
|
151
|
+
console.log(`\n✨ Tokens saved to ${outputFile}`)
|
|
152
|
+
console.log('\nYou can now use these environment variables:')
|
|
153
|
+
console.log(' - YOTO_ACCESS_TOKEN')
|
|
154
|
+
console.log(' - YOTO_REFRESH_TOKEN')
|
|
155
|
+
console.log(' - YOTO_CLIENT_ID')
|
|
156
|
+
|
|
157
|
+
process.exit(0)
|
|
158
|
+
} catch (error) {
|
|
159
|
+
handleCliError(error)
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Run the main function
|
|
164
|
+
await main().catch(err => {
|
|
165
|
+
console.error('\n❌ Unexpected error:')
|
|
166
|
+
console.error(err)
|
|
167
|
+
process.exit(1)
|
|
168
|
+
})
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"token-info.d.ts","sourceRoot":"","sources":["token-info.js"],"names":[],"mappings":""}
|