@factorialco/fcode-cli 1.5.0 → 2.0.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 +3 -3
- package/dist/commands/http.d.ts +6 -4
- package/dist/commands/http.js +40 -24
- package/dist/commands/team/push.js +6 -1
- package/dist/core/types.d.ts +28 -3
- package/dist/core/types.js +14 -4
- package/dist/repositories/team.d.ts +3 -1
- package/dist/repositories/team.js +19 -3
- package/dist/services/auth-service.d.ts +3 -0
- package/dist/services/auth-service.js +56 -5
- package/dist/services/fcode-graphql-client.d.ts +10 -0
- package/dist/services/fcode-graphql-client.js +68 -26
- package/dist/services/oauth-browser-service.js +1 -0
- package/dist/services/webhook-auth.d.ts +21 -0
- package/dist/services/webhook-auth.js +77 -0
- package/oclif.manifest.json +236 -236
- package/package.json +1 -1
|
@@ -144,6 +144,7 @@ async function loginViaBrowser(url, authUrl, clientId) {
|
|
|
144
144
|
const token = {
|
|
145
145
|
access_token: tokenSet.access_token ?? '',
|
|
146
146
|
refresh_token: tokenSet.refresh_token ?? '',
|
|
147
|
+
expires_at: tokenSet.expires_at,
|
|
147
148
|
};
|
|
148
149
|
const userInfo = await auth_service_1.default.getUserInfo(normalizedAuthUrl, token);
|
|
149
150
|
const authInfo = {
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type * as http from 'node:http';
|
|
2
|
+
import { ProcessWebhook, WebhookAuth } from '../core/types';
|
|
3
|
+
export type WebhookAuthResult = {
|
|
4
|
+
authorized: true;
|
|
5
|
+
} | {
|
|
6
|
+
authorized: false;
|
|
7
|
+
reason: string;
|
|
8
|
+
};
|
|
9
|
+
/**
|
|
10
|
+
* Node lower-cases incoming header names, while a configured name is stored as the user typed it,
|
|
11
|
+
* so the lookup has to be case-insensitive. A repeated header arrives as an array and is rejected
|
|
12
|
+
* rather than joined: a spliced value must never authenticate.
|
|
13
|
+
*/
|
|
14
|
+
export declare function findHeader(headers: http.IncomingHttpHeaders, name: string): string | undefined;
|
|
15
|
+
/**
|
|
16
|
+
* The configuration a webhook actually enforces: its own when CUSTOM, the workspace's when TEAM.
|
|
17
|
+
* Returns null only when the webhook is public — TEAM without a workspace configuration throws,
|
|
18
|
+
* because treating it as public would silently unprotect a webhook its owner chose to protect.
|
|
19
|
+
*/
|
|
20
|
+
export declare function resolveEffectiveAuth(webhook: ProcessWebhook | undefined, teamWebhookAuth: WebhookAuth | undefined): WebhookAuth | null;
|
|
21
|
+
export declare function checkWebhookAuth(effectiveAuth: WebhookAuth | null, headers: http.IncomingHttpHeaders, variableValue: string | undefined): WebhookAuthResult;
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.findHeader = findHeader;
|
|
4
|
+
exports.resolveEffectiveAuth = resolveEffectiveAuth;
|
|
5
|
+
exports.checkWebhookAuth = checkWebhookAuth;
|
|
6
|
+
const crypto = require("node:crypto");
|
|
7
|
+
const types_1 = require("../core/types");
|
|
8
|
+
/**
|
|
9
|
+
* Mirrors the cloud's webhook authentication so `fcode http` accepts and rejects exactly what the
|
|
10
|
+
* platform does. The scheme in the default Authorization header is case-insensitive per RFC 7235;
|
|
11
|
+
* the token is not. A custom header carries the raw value, with no scheme to parse.
|
|
12
|
+
*/
|
|
13
|
+
const BEARER_PATTERN = /^Bearer[ \t]+(\S+)[ \t]*$/i;
|
|
14
|
+
/**
|
|
15
|
+
* Node lower-cases incoming header names, while a configured name is stored as the user typed it,
|
|
16
|
+
* so the lookup has to be case-insensitive. A repeated header arrives as an array and is rejected
|
|
17
|
+
* rather than joined: a spliced value must never authenticate.
|
|
18
|
+
*/
|
|
19
|
+
function findHeader(headers, name) {
|
|
20
|
+
const lowerCased = name.toLowerCase();
|
|
21
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
22
|
+
if (key.toLowerCase() === lowerCased) {
|
|
23
|
+
return typeof value === 'string' ? value : undefined;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return undefined;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* The configuration a webhook actually enforces: its own when CUSTOM, the workspace's when TEAM.
|
|
30
|
+
* Returns null only when the webhook is public — TEAM without a workspace configuration throws,
|
|
31
|
+
* because treating it as public would silently unprotect a webhook its owner chose to protect.
|
|
32
|
+
*/
|
|
33
|
+
function resolveEffectiveAuth(webhook, teamWebhookAuth) {
|
|
34
|
+
const authMode = webhook?.authMode;
|
|
35
|
+
if (!authMode || authMode === 'NONE') {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
if (authMode === 'TEAM') {
|
|
39
|
+
if (!teamWebhookAuth) {
|
|
40
|
+
throw new Error('this webhook inherits the workspace webhook auth, which is not set in team.json');
|
|
41
|
+
}
|
|
42
|
+
return teamWebhookAuth;
|
|
43
|
+
}
|
|
44
|
+
if (!webhook?.auth?.variableKey) {
|
|
45
|
+
throw new Error('this webhook uses the CUSTOM auth mode but names no variable in metadata.json');
|
|
46
|
+
}
|
|
47
|
+
return webhook.auth;
|
|
48
|
+
}
|
|
49
|
+
/** Constant-time over equal-length digests, so neither the token nor its length leaks. */
|
|
50
|
+
function matches(expectedToken, givenToken) {
|
|
51
|
+
const digest = (value) => crypto.createHash('sha256').update(value, 'utf8').digest();
|
|
52
|
+
return crypto.timingSafeEqual(digest(expectedToken), digest(givenToken));
|
|
53
|
+
}
|
|
54
|
+
function checkWebhookAuth(effectiveAuth, headers, variableValue) {
|
|
55
|
+
if (effectiveAuth === null) {
|
|
56
|
+
return { authorized: true };
|
|
57
|
+
}
|
|
58
|
+
const headerName = effectiveAuth.headerName ?? types_1.DEFAULT_WEBHOOK_AUTH_HEADER;
|
|
59
|
+
const headerValue = findHeader(headers, headerName);
|
|
60
|
+
if (headerValue === undefined || headerValue.trim().length === 0) {
|
|
61
|
+
return { authorized: false, reason: `the ${headerName} header is missing` };
|
|
62
|
+
}
|
|
63
|
+
let givenToken = headerValue;
|
|
64
|
+
if (headerName.toLowerCase() === types_1.DEFAULT_WEBHOOK_AUTH_HEADER.toLowerCase()) {
|
|
65
|
+
givenToken = BEARER_PATTERN.exec(headerValue)?.[1];
|
|
66
|
+
if (givenToken === undefined) {
|
|
67
|
+
return { authorized: false, reason: `the ${headerName} header is not a bearer token` };
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
if (variableValue === undefined) {
|
|
71
|
+
return { authorized: false, reason: `the ${effectiveAuth.variableKey} variable is not defined` };
|
|
72
|
+
}
|
|
73
|
+
if (!matches(variableValue, givenToken)) {
|
|
74
|
+
return { authorized: false, reason: `the value sent in ${headerName} does not match ${effectiveAuth.variableKey}` };
|
|
75
|
+
}
|
|
76
|
+
return { authorized: true };
|
|
77
|
+
}
|