@tamyla/clodo-framework 4.3.1 → 4.3.3
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/CHANGELOG.md +14 -0
- package/package.json +1 -1
- package/scripts/x-oauth1.cjs +117 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,17 @@
|
|
|
1
|
+
## [4.3.3](https://github.com/tamylaa/clodo-framework/compare/v4.3.2...v4.3.3) (2026-02-02)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Bug Fixes
|
|
5
|
+
|
|
6
|
+
* **x:** strip CRLF from POST_MESSAGE lines before comparison and message extraction ([d9db8e0](https://github.com/tamylaa/clodo-framework/commit/d9db8e0eb87081c5e9506191b21a32970a7745f4))
|
|
7
|
+
|
|
8
|
+
## [4.3.2](https://github.com/tamylaa/clodo-framework/compare/v4.3.1...v4.3.2) (2026-02-02)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Bug Fixes
|
|
12
|
+
|
|
13
|
+
* **x:** resolve YAML syntax errors in run-validate-now.yml by quoting step name and adjusting indentation ([3eecf16](https://github.com/tamylaa/clodo-framework/commit/3eecf1690746c86c077f31fb64596da6d3ff9cf1))
|
|
14
|
+
|
|
1
15
|
## [4.3.1](https://github.com/tamylaa/clodo-framework/compare/v4.3.0...v4.3.1) (2026-02-01)
|
|
2
16
|
|
|
3
17
|
|
package/package.json
CHANGED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/*
|
|
3
|
+
Simple OAuth 1.0a helper for X API (Twitter) — no external deps.
|
|
4
|
+
Usage:
|
|
5
|
+
node scripts/x-oauth1.js --dry-check
|
|
6
|
+
node scripts/x-oauth1.js --post "message text"
|
|
7
|
+
|
|
8
|
+
Reads these env vars from GitHub Actions secrets or local env:
|
|
9
|
+
X_API_KEY, X_API_SECRET, X_ACCESS_TOKEN, X_ACCESS_SECRET
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const crypto = require('crypto');
|
|
13
|
+
const { URLSearchParams } = require('url');
|
|
14
|
+
|
|
15
|
+
function percentEncode(str){
|
|
16
|
+
return encodeURIComponent(str)
|
|
17
|
+
.replace(/[!*()']/g, c => `%${c.charCodeAt(0).toString(16).toUpperCase()}`);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function generateNonce(length = 32){
|
|
21
|
+
return crypto.randomBytes(length).toString('base64').replace(/[^a-zA-Z0-9]/g, '').slice(0, 32);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function timestamp(){
|
|
25
|
+
return Math.floor(Date.now() / 1000).toString();
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function buildSignature(method, baseUrl, params, consumerSecret, tokenSecret){
|
|
29
|
+
const sorted = Object.keys(params).sort().map(k => `${percentEncode(k)}=${percentEncode(params[k])}`).join('&');
|
|
30
|
+
const baseString = [method.toUpperCase(), percentEncode(baseUrl), percentEncode(sorted)].join('&');
|
|
31
|
+
const signingKey = `${percentEncode(consumerSecret)}&${tokenSecret ? percentEncode(tokenSecret) : ''}`;
|
|
32
|
+
const hmac = crypto.createHmac('sha1', signingKey).update(baseString).digest('base64');
|
|
33
|
+
return hmac;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function buildAuthHeader(params){
|
|
37
|
+
const header = 'OAuth ' + Object.keys(params).sort().map(k => `${percentEncode(k)}="${percentEncode(params[k])}"`).join(', ');
|
|
38
|
+
return header;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async function httpRequest(method, url, { body = null, extraParams = {} } = {}){
|
|
42
|
+
const consumerKey = process.env.X_API_KEY;
|
|
43
|
+
const consumerSecret = process.env.X_API_SECRET;
|
|
44
|
+
const accessToken = process.env.X_ACCESS_TOKEN;
|
|
45
|
+
const accessSecret = process.env.X_ACCESS_SECRET;
|
|
46
|
+
|
|
47
|
+
if(!consumerKey || !consumerSecret || !accessToken || !accessSecret){
|
|
48
|
+
console.error('Missing required X API credentials in env (X_API_KEY, X_API_SECRET, X_ACCESS_TOKEN, X_ACCESS_SECRET)');
|
|
49
|
+
process.exit(2);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const oauth = {
|
|
53
|
+
oauth_consumer_key: consumerKey,
|
|
54
|
+
oauth_nonce: generateNonce(),
|
|
55
|
+
oauth_signature_method: 'HMAC-SHA1',
|
|
56
|
+
oauth_timestamp: timestamp(),
|
|
57
|
+
oauth_token: accessToken,
|
|
58
|
+
oauth_version: '1.0'
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const urlObj = new URL(url);
|
|
62
|
+
const queryParams = Object.fromEntries(urlObj.searchParams.entries());
|
|
63
|
+
|
|
64
|
+
const allParams = Object.assign({}, queryParams, extraParams, oauth);
|
|
65
|
+
// if body is JSON and method POST, do not include body in signature unless twitter requires; for tweets, parameters are in body as json (we won't include)
|
|
66
|
+
|
|
67
|
+
const signature = buildSignature(method, urlObj.origin + urlObj.pathname, allParams, consumerSecret, accessSecret);
|
|
68
|
+
oauth.oauth_signature = signature;
|
|
69
|
+
|
|
70
|
+
const headers = {
|
|
71
|
+
Authorization: buildAuthHeader(oauth),
|
|
72
|
+
'Content-Type': 'application/json'
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const fetch = globalThis.fetch || require('node-fetch');
|
|
76
|
+
const opts = { method, headers };
|
|
77
|
+
if(body){ opts.body = JSON.stringify(body); }
|
|
78
|
+
|
|
79
|
+
const resp = await fetch(url, opts);
|
|
80
|
+
const text = await resp.text();
|
|
81
|
+
let json;
|
|
82
|
+
try{ json = JSON.parse(text); } catch(e){ json = text; }
|
|
83
|
+
return { status: resp.status, body: json };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async function dryCheck(){
|
|
87
|
+
const url = 'https://api.twitter.com/2/users/me';
|
|
88
|
+
console.log('Calling GET', url);
|
|
89
|
+
const r = await httpRequest('GET', url);
|
|
90
|
+
console.log('HTTP status:', r.status);
|
|
91
|
+
console.log('Body:', JSON.stringify(r.body, null, 2));
|
|
92
|
+
process.exit(r.status === 200 ? 0 : 1);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async function postTweet(message){
|
|
96
|
+
const url = 'https://api.twitter.com/2/tweets';
|
|
97
|
+
console.log('POST to', url);
|
|
98
|
+
const r = await httpRequest('POST', url, { body: { text: message } });
|
|
99
|
+
console.log('HTTP status:', r.status);
|
|
100
|
+
console.log('Body:', JSON.stringify(r.body, null, 2));
|
|
101
|
+
process.exit(r.status >=200 && r.status < 300 ? 0 : 1);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
(async ()=>{
|
|
105
|
+
const args = process.argv.slice(2);
|
|
106
|
+
if(args.includes('--dry-check')){
|
|
107
|
+
await dryCheck();
|
|
108
|
+
} else if(args.includes('--post')){
|
|
109
|
+
const i = args.indexOf('--post');
|
|
110
|
+
const message = args[i+1] || '';
|
|
111
|
+
if(!message){ console.error('Provide message after --post'); process.exit(2); }
|
|
112
|
+
await postTweet(message);
|
|
113
|
+
} else {
|
|
114
|
+
console.error('Usage: --dry-check OR --post "message"');
|
|
115
|
+
process.exit(2);
|
|
116
|
+
}
|
|
117
|
+
})();
|