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.
Files changed (92) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +736 -0
  3. package/bin/auth.d.ts +3 -0
  4. package/bin/auth.d.ts.map +1 -0
  5. package/bin/auth.js +130 -0
  6. package/bin/content.d.ts +3 -0
  7. package/bin/content.d.ts.map +1 -0
  8. package/bin/content.js +117 -0
  9. package/bin/devices.d.ts +3 -0
  10. package/bin/devices.d.ts.map +1 -0
  11. package/bin/devices.js +239 -0
  12. package/bin/groups.d.ts +3 -0
  13. package/bin/groups.d.ts.map +1 -0
  14. package/bin/groups.js +80 -0
  15. package/bin/icons.d.ts +3 -0
  16. package/bin/icons.d.ts.map +1 -0
  17. package/bin/icons.js +100 -0
  18. package/bin/lib/cli-helpers.d.ts +21 -0
  19. package/bin/lib/cli-helpers.d.ts.map +1 -0
  20. package/bin/lib/cli-helpers.js +140 -0
  21. package/bin/lib/token-helpers.d.ts +14 -0
  22. package/bin/lib/token-helpers.d.ts.map +1 -0
  23. package/bin/lib/token-helpers.js +151 -0
  24. package/bin/refresh-token.d.ts +3 -0
  25. package/bin/refresh-token.d.ts.map +1 -0
  26. package/bin/refresh-token.js +168 -0
  27. package/bin/token-info.d.ts +3 -0
  28. package/bin/token-info.d.ts.map +1 -0
  29. package/bin/token-info.js +351 -0
  30. package/index.d.ts +218 -0
  31. package/index.d.ts.map +1 -0
  32. package/index.js +689 -0
  33. package/lib/api-endpoints/auth.d.ts +56 -0
  34. package/lib/api-endpoints/auth.d.ts.map +1 -0
  35. package/lib/api-endpoints/auth.js +209 -0
  36. package/lib/api-endpoints/auth.test.js +27 -0
  37. package/lib/api-endpoints/constants.d.ts +6 -0
  38. package/lib/api-endpoints/constants.d.ts.map +1 -0
  39. package/lib/api-endpoints/constants.js +31 -0
  40. package/lib/api-endpoints/content.d.ts +275 -0
  41. package/lib/api-endpoints/content.d.ts.map +1 -0
  42. package/lib/api-endpoints/content.js +518 -0
  43. package/lib/api-endpoints/content.test.js +250 -0
  44. package/lib/api-endpoints/devices.d.ts +202 -0
  45. package/lib/api-endpoints/devices.d.ts.map +1 -0
  46. package/lib/api-endpoints/devices.js +404 -0
  47. package/lib/api-endpoints/devices.test.js +483 -0
  48. package/lib/api-endpoints/family-library-groups.d.ts +75 -0
  49. package/lib/api-endpoints/family-library-groups.d.ts.map +1 -0
  50. package/lib/api-endpoints/family-library-groups.js +247 -0
  51. package/lib/api-endpoints/family-library-groups.test.js +272 -0
  52. package/lib/api-endpoints/family.d.ts +39 -0
  53. package/lib/api-endpoints/family.d.ts.map +1 -0
  54. package/lib/api-endpoints/family.js +166 -0
  55. package/lib/api-endpoints/family.test.js +184 -0
  56. package/lib/api-endpoints/helpers.d.ts +29 -0
  57. package/lib/api-endpoints/helpers.d.ts.map +1 -0
  58. package/lib/api-endpoints/helpers.js +104 -0
  59. package/lib/api-endpoints/icons.d.ts +62 -0
  60. package/lib/api-endpoints/icons.d.ts.map +1 -0
  61. package/lib/api-endpoints/icons.js +201 -0
  62. package/lib/api-endpoints/icons.test.js +118 -0
  63. package/lib/api-endpoints/media.d.ts +37 -0
  64. package/lib/api-endpoints/media.d.ts.map +1 -0
  65. package/lib/api-endpoints/media.js +155 -0
  66. package/lib/api-endpoints/test-helpers.d.ts +7 -0
  67. package/lib/api-endpoints/test-helpers.d.ts.map +1 -0
  68. package/lib/api-endpoints/test-helpers.js +64 -0
  69. package/lib/mqtt/client.d.ts +124 -0
  70. package/lib/mqtt/client.d.ts.map +1 -0
  71. package/lib/mqtt/client.js +558 -0
  72. package/lib/mqtt/commands.d.ts +69 -0
  73. package/lib/mqtt/commands.d.ts.map +1 -0
  74. package/lib/mqtt/commands.js +238 -0
  75. package/lib/mqtt/factory.d.ts +12 -0
  76. package/lib/mqtt/factory.d.ts.map +1 -0
  77. package/lib/mqtt/factory.js +107 -0
  78. package/lib/mqtt/index.d.ts +5 -0
  79. package/lib/mqtt/index.d.ts.map +1 -0
  80. package/lib/mqtt/index.js +81 -0
  81. package/lib/mqtt/mqtt.test.js +168 -0
  82. package/lib/mqtt/topics.d.ts +34 -0
  83. package/lib/mqtt/topics.d.ts.map +1 -0
  84. package/lib/mqtt/topics.js +295 -0
  85. package/lib/pkg.cjs +3 -0
  86. package/lib/pkg.d.cts +70 -0
  87. package/lib/pkg.d.cts.map +1 -0
  88. package/lib/token.d.ts +29 -0
  89. package/lib/token.d.ts.map +1 -0
  90. package/lib/token.js +240 -0
  91. package/package.json +91 -0
  92. 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,3 @@
1
+ #!/usr/bin/env node
2
+ export {};
3
+ //# sourceMappingURL=refresh-token.d.ts.map
@@ -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,3 @@
1
+ #!/usr/bin/env node
2
+ export {};
3
+ //# sourceMappingURL=token-info.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"token-info.d.ts","sourceRoot":"","sources":["token-info.js"],"names":[],"mappings":""}