@crewx/shared 0.0.6-rc.20 → 0.0.6-rc.22
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/crewx-api-credential.d.ts +14 -0
- package/crewx-api-credential.js +135 -0
- package/crewx-api-credential.ts +131 -0
- package/package.json +31 -2
- package/tests/crewx-api-credential.test.ts +110 -0
- package/tsconfig.json +18 -0
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export type RuntimeCredentialPurpose = 'notify' | 'wi' | 'chromex-negotiate' | 'pairing';
|
|
2
|
+
export interface ResolvedCredential {
|
|
3
|
+
token: string;
|
|
4
|
+
source: 'env' | 'file';
|
|
5
|
+
}
|
|
6
|
+
type CrewxCredentialErrorCode = 'MISSING' | 'BAD_MODE' | 'UNREADABLE';
|
|
7
|
+
export declare class CrewxCredentialError extends Error {
|
|
8
|
+
readonly code: CrewxCredentialErrorCode;
|
|
9
|
+
readonly exitCode: number;
|
|
10
|
+
constructor(code: CrewxCredentialErrorCode, message: string);
|
|
11
|
+
}
|
|
12
|
+
export declare function resolveCrewxApiCredential(purpose: RuntimeCredentialPurpose, env?: NodeJS.ProcessEnv): ResolvedCredential;
|
|
13
|
+
export declare function authorizationHeader(cred: ResolvedCredential): Record<'Authorization', string>;
|
|
14
|
+
export {};
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.CrewxCredentialError = void 0;
|
|
37
|
+
exports.resolveCrewxApiCredential = resolveCrewxApiCredential;
|
|
38
|
+
exports.authorizationHeader = authorizationHeader;
|
|
39
|
+
const fs = __importStar(require("node:fs"));
|
|
40
|
+
const path = __importStar(require("node:path"));
|
|
41
|
+
const sdk_1 = require("@crewx/sdk");
|
|
42
|
+
const RUNTIME_CREDENTIAL_PURPOSES = new Set([
|
|
43
|
+
'notify',
|
|
44
|
+
'wi',
|
|
45
|
+
'chromex-negotiate',
|
|
46
|
+
'pairing',
|
|
47
|
+
]);
|
|
48
|
+
// These are sysexits-style values so shell callers can distinguish the local
|
|
49
|
+
// credential failure before an HTTP request is attempted.
|
|
50
|
+
const EXIT_CODES = {
|
|
51
|
+
MISSING: 78,
|
|
52
|
+
BAD_MODE: 77,
|
|
53
|
+
UNREADABLE: 74,
|
|
54
|
+
};
|
|
55
|
+
const HEX_64 = '[0-9a-f]{64}';
|
|
56
|
+
class CrewxCredentialError extends Error {
|
|
57
|
+
code;
|
|
58
|
+
exitCode;
|
|
59
|
+
constructor(code, message) {
|
|
60
|
+
super(message);
|
|
61
|
+
this.name = 'CrewxCredentialError';
|
|
62
|
+
this.code = code;
|
|
63
|
+
this.exitCode = EXIT_CODES[code];
|
|
64
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
exports.CrewxCredentialError = CrewxCredentialError;
|
|
68
|
+
function isErrnoCode(error, code) {
|
|
69
|
+
return typeof error === 'object'
|
|
70
|
+
&& error !== null
|
|
71
|
+
&& 'code' in error
|
|
72
|
+
&& error.code === code;
|
|
73
|
+
}
|
|
74
|
+
function credentialPath(purpose) {
|
|
75
|
+
return path.join((0, sdk_1.getCrewxHome)(), 'auth', 'runtime', `${purpose}.token`);
|
|
76
|
+
}
|
|
77
|
+
function invalidPurpose(purpose) {
|
|
78
|
+
throw new TypeError(`Unsupported runtime credential purpose: ${String(purpose)}`);
|
|
79
|
+
}
|
|
80
|
+
function missingCredential(purpose, filePath) {
|
|
81
|
+
throw new CrewxCredentialError('MISSING', `Runtime credential '${purpose}' is missing at ${filePath}. `
|
|
82
|
+
+ 'Run the CrewX server once to create it or set CREWX_API_TOKEN.');
|
|
83
|
+
}
|
|
84
|
+
function badMode(purpose, filePath) {
|
|
85
|
+
throw new CrewxCredentialError('BAD_MODE', `Runtime credential '${purpose}' at ${filePath} has insecure permissions; expected mode 0600.`);
|
|
86
|
+
}
|
|
87
|
+
function unreadableCredential(purpose, filePath) {
|
|
88
|
+
throw new CrewxCredentialError('UNREADABLE', `Runtime credential '${purpose}' at ${filePath} is unreadable or invalid. `
|
|
89
|
+
+ 'Check the file contents and permissions.');
|
|
90
|
+
}
|
|
91
|
+
function readRuntimeCredential(purpose) {
|
|
92
|
+
const filePath = credentialPath(purpose);
|
|
93
|
+
let stats;
|
|
94
|
+
try {
|
|
95
|
+
// lstat prevents a symlink from bypassing the mode check on POSIX.
|
|
96
|
+
stats = fs.lstatSync(filePath);
|
|
97
|
+
}
|
|
98
|
+
catch (error) {
|
|
99
|
+
if (isErrnoCode(error, 'ENOENT'))
|
|
100
|
+
missingCredential(purpose, filePath);
|
|
101
|
+
unreadableCredential(purpose, filePath);
|
|
102
|
+
}
|
|
103
|
+
if (!stats.isFile())
|
|
104
|
+
unreadableCredential(purpose, filePath);
|
|
105
|
+
// Windows does not expose POSIX mode semantics; the server contract skips
|
|
106
|
+
// this check there as well.
|
|
107
|
+
if (process.platform !== 'win32' && (stats.mode & 0o777) !== 0o600) {
|
|
108
|
+
badMode(purpose, filePath);
|
|
109
|
+
}
|
|
110
|
+
let token;
|
|
111
|
+
try {
|
|
112
|
+
token = fs.readFileSync(filePath, 'utf8');
|
|
113
|
+
}
|
|
114
|
+
catch (error) {
|
|
115
|
+
if (isErrnoCode(error, 'ENOENT'))
|
|
116
|
+
missingCredential(purpose, filePath);
|
|
117
|
+
unreadableCredential(purpose, filePath);
|
|
118
|
+
}
|
|
119
|
+
const expected = new RegExp(`^crewxrt_${purpose}_${HEX_64}$`);
|
|
120
|
+
if (!expected.test(token))
|
|
121
|
+
unreadableCredential(purpose, filePath);
|
|
122
|
+
return token;
|
|
123
|
+
}
|
|
124
|
+
function resolveCrewxApiCredential(purpose, env = process.env) {
|
|
125
|
+
if (!RUNTIME_CREDENTIAL_PURPOSES.has(purpose))
|
|
126
|
+
invalidPurpose(purpose);
|
|
127
|
+
const envToken = env['CREWX_API_TOKEN'];
|
|
128
|
+
if (typeof envToken === 'string' && envToken.length > 0) {
|
|
129
|
+
return { token: envToken, source: 'env' };
|
|
130
|
+
}
|
|
131
|
+
return { token: readRuntimeCredential(purpose), source: 'file' };
|
|
132
|
+
}
|
|
133
|
+
function authorizationHeader(cred) {
|
|
134
|
+
return { Authorization: `Bearer ${cred.token}` };
|
|
135
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import * as fs from 'node:fs';
|
|
2
|
+
import * as path from 'node:path';
|
|
3
|
+
import { getCrewxHome } from '@crewx/sdk';
|
|
4
|
+
|
|
5
|
+
export type RuntimeCredentialPurpose = 'notify' | 'wi' | 'chromex-negotiate' | 'pairing';
|
|
6
|
+
|
|
7
|
+
export interface ResolvedCredential {
|
|
8
|
+
token: string;
|
|
9
|
+
source: 'env' | 'file';
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
type CrewxCredentialErrorCode = 'MISSING' | 'BAD_MODE' | 'UNREADABLE';
|
|
13
|
+
|
|
14
|
+
const RUNTIME_CREDENTIAL_PURPOSES = new Set<RuntimeCredentialPurpose>([
|
|
15
|
+
'notify',
|
|
16
|
+
'wi',
|
|
17
|
+
'chromex-negotiate',
|
|
18
|
+
'pairing',
|
|
19
|
+
]);
|
|
20
|
+
|
|
21
|
+
// These are sysexits-style values so shell callers can distinguish the local
|
|
22
|
+
// credential failure before an HTTP request is attempted.
|
|
23
|
+
const EXIT_CODES: Record<CrewxCredentialErrorCode, number> = {
|
|
24
|
+
MISSING: 78,
|
|
25
|
+
BAD_MODE: 77,
|
|
26
|
+
UNREADABLE: 74,
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const HEX_64 = '[0-9a-f]{64}';
|
|
30
|
+
|
|
31
|
+
export class CrewxCredentialError extends Error {
|
|
32
|
+
readonly code: CrewxCredentialErrorCode;
|
|
33
|
+
readonly exitCode: number;
|
|
34
|
+
|
|
35
|
+
constructor(code: CrewxCredentialErrorCode, message: string) {
|
|
36
|
+
super(message);
|
|
37
|
+
this.name = 'CrewxCredentialError';
|
|
38
|
+
this.code = code;
|
|
39
|
+
this.exitCode = EXIT_CODES[code];
|
|
40
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function isErrnoCode(error: unknown, code: string): boolean {
|
|
45
|
+
return typeof error === 'object'
|
|
46
|
+
&& error !== null
|
|
47
|
+
&& 'code' in error
|
|
48
|
+
&& (error as { code?: unknown }).code === code;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function credentialPath(purpose: RuntimeCredentialPurpose): string {
|
|
52
|
+
return path.join(getCrewxHome(), 'auth', 'runtime', `${purpose}.token`);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function invalidPurpose(purpose: RuntimeCredentialPurpose): never {
|
|
56
|
+
throw new TypeError(`Unsupported runtime credential purpose: ${String(purpose)}`);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function missingCredential(purpose: RuntimeCredentialPurpose, filePath: string): never {
|
|
60
|
+
throw new CrewxCredentialError(
|
|
61
|
+
'MISSING',
|
|
62
|
+
`Runtime credential '${purpose}' is missing at ${filePath}. `
|
|
63
|
+
+ 'Run the CrewX server once to create it or set CREWX_API_TOKEN.',
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function badMode(purpose: RuntimeCredentialPurpose, filePath: string): never {
|
|
68
|
+
throw new CrewxCredentialError(
|
|
69
|
+
'BAD_MODE',
|
|
70
|
+
`Runtime credential '${purpose}' at ${filePath} has insecure permissions; expected mode 0600.`,
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function unreadableCredential(purpose: RuntimeCredentialPurpose, filePath: string): never {
|
|
75
|
+
throw new CrewxCredentialError(
|
|
76
|
+
'UNREADABLE',
|
|
77
|
+
`Runtime credential '${purpose}' at ${filePath} is unreadable or invalid. `
|
|
78
|
+
+ 'Check the file contents and permissions.',
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function readRuntimeCredential(purpose: RuntimeCredentialPurpose): string {
|
|
83
|
+
const filePath = credentialPath(purpose);
|
|
84
|
+
let stats: fs.Stats;
|
|
85
|
+
|
|
86
|
+
try {
|
|
87
|
+
// lstat prevents a symlink from bypassing the mode check on POSIX.
|
|
88
|
+
stats = fs.lstatSync(filePath);
|
|
89
|
+
} catch (error: unknown) {
|
|
90
|
+
if (isErrnoCode(error, 'ENOENT')) missingCredential(purpose, filePath);
|
|
91
|
+
unreadableCredential(purpose, filePath);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (!stats!.isFile()) unreadableCredential(purpose, filePath);
|
|
95
|
+
|
|
96
|
+
// Windows does not expose POSIX mode semantics; the server contract skips
|
|
97
|
+
// this check there as well.
|
|
98
|
+
if (process.platform !== 'win32' && (stats!.mode & 0o777) !== 0o600) {
|
|
99
|
+
badMode(purpose, filePath);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
let token: string;
|
|
103
|
+
try {
|
|
104
|
+
token = fs.readFileSync(filePath, 'utf8');
|
|
105
|
+
} catch (error: unknown) {
|
|
106
|
+
if (isErrnoCode(error, 'ENOENT')) missingCredential(purpose, filePath);
|
|
107
|
+
unreadableCredential(purpose, filePath);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const expected = new RegExp(`^crewxrt_${purpose}_${HEX_64}$`);
|
|
111
|
+
if (!expected.test(token!)) unreadableCredential(purpose, filePath);
|
|
112
|
+
return token!;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function resolveCrewxApiCredential(
|
|
116
|
+
purpose: RuntimeCredentialPurpose,
|
|
117
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
118
|
+
): ResolvedCredential {
|
|
119
|
+
if (!RUNTIME_CREDENTIAL_PURPOSES.has(purpose)) invalidPurpose(purpose);
|
|
120
|
+
|
|
121
|
+
const envToken = env['CREWX_API_TOKEN'];
|
|
122
|
+
if (typeof envToken === 'string' && envToken.length > 0) {
|
|
123
|
+
return { token: envToken, source: 'env' };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return { token: readRuntimeCredential(purpose), source: 'file' };
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export function authorizationHeader(cred: ResolvedCredential): Record<'Authorization', string> {
|
|
130
|
+
return { Authorization: `Bearer ${cred.token}` };
|
|
131
|
+
}
|
package/package.json
CHANGED
|
@@ -1,12 +1,41 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@crewx/shared",
|
|
3
|
-
"version": "0.0.6-rc.
|
|
3
|
+
"version": "0.0.6-rc.22",
|
|
4
4
|
"main": "skill-tracer.js",
|
|
5
|
+
"exports": {
|
|
6
|
+
".": {
|
|
7
|
+
"types": "./skill-tracer.d.ts",
|
|
8
|
+
"require": "./skill-tracer.js",
|
|
9
|
+
"default": "./skill-tracer.js"
|
|
10
|
+
},
|
|
11
|
+
"./skill-tracer": {
|
|
12
|
+
"types": "./skill-tracer.d.ts",
|
|
13
|
+
"require": "./skill-tracer.js",
|
|
14
|
+
"default": "./skill-tracer.js"
|
|
15
|
+
},
|
|
16
|
+
"./crewx-api-credential": {
|
|
17
|
+
"types": "./crewx-api-credential.d.ts",
|
|
18
|
+
"require": "./crewx-api-credential.js",
|
|
19
|
+
"default": "./crewx-api-credential.js"
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
"typesVersions": {
|
|
23
|
+
"*": {
|
|
24
|
+
"crewx-api-credential": [
|
|
25
|
+
"crewx-api-credential.d.ts"
|
|
26
|
+
]
|
|
27
|
+
}
|
|
28
|
+
},
|
|
5
29
|
"description": "Shared utilities for CrewX built-in packages",
|
|
6
30
|
"dependencies": {
|
|
7
|
-
"@crewx/sdk": "0.
|
|
31
|
+
"@crewx/sdk": "0.9.0-rc.107"
|
|
32
|
+
},
|
|
33
|
+
"devDependencies": {
|
|
34
|
+
"@types/node": "^20.0.0",
|
|
35
|
+
"typescript": "^5.0.0"
|
|
8
36
|
},
|
|
9
37
|
"scripts": {
|
|
38
|
+
"build": "tsc -p tsconfig.json",
|
|
10
39
|
"test": "echo no tests"
|
|
11
40
|
}
|
|
12
41
|
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
// @vitest-environment node
|
|
2
|
+
|
|
3
|
+
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
4
|
+
import * as fs from 'node:fs';
|
|
5
|
+
import * as os from 'node:os';
|
|
6
|
+
import * as path from 'node:path';
|
|
7
|
+
import {
|
|
8
|
+
authorizationHeader,
|
|
9
|
+
CrewxCredentialError,
|
|
10
|
+
resolveCrewxApiCredential,
|
|
11
|
+
} from '../crewx-api-credential';
|
|
12
|
+
|
|
13
|
+
const VALID_HEX = 'a'.repeat(64);
|
|
14
|
+
|
|
15
|
+
let tmpHome: string;
|
|
16
|
+
const savedEnv = { ...process.env };
|
|
17
|
+
|
|
18
|
+
function runtimePath(purpose: string): string {
|
|
19
|
+
return path.join(tmpHome, 'auth', 'runtime', `${purpose}.token`);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function writeCredential(purpose: string, mode = 0o600, token = `crewxrt_${purpose}_${VALID_HEX}`): void {
|
|
23
|
+
const file = runtimePath(purpose);
|
|
24
|
+
fs.mkdirSync(path.dirname(file), { recursive: true, mode: 0o700 });
|
|
25
|
+
fs.writeFileSync(file, token, { encoding: 'utf8', mode });
|
|
26
|
+
fs.chmodSync(file, mode);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
beforeEach(() => {
|
|
30
|
+
tmpHome = fs.mkdtempSync(path.join(os.tmpdir(), 'crewx-runtime-credential-'));
|
|
31
|
+
process.env['CREWX_HOME'] = tmpHome;
|
|
32
|
+
delete process.env['CREWX_API_TOKEN'];
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
afterEach(() => {
|
|
36
|
+
process.env = { ...savedEnv };
|
|
37
|
+
fs.rmSync(tmpHome, { recursive: true, force: true });
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
describe('resolveCrewxApiCredential', () => {
|
|
41
|
+
it('resolves a purpose-scoped 0600 runtime credential and builds its Bearer header', () => {
|
|
42
|
+
writeCredential('notify');
|
|
43
|
+
|
|
44
|
+
const credential = resolveCrewxApiCredential('notify');
|
|
45
|
+
|
|
46
|
+
expect(credential).toEqual({ token: `crewxrt_notify_${VALID_HEX}`, source: 'file' });
|
|
47
|
+
expect(authorizationHeader(credential)).toEqual({
|
|
48
|
+
Authorization: `Bearer crewxrt_notify_${VALID_HEX}`,
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('resolves the pairing runtime credential', () => {
|
|
53
|
+
writeCredential('pairing');
|
|
54
|
+
|
|
55
|
+
expect(resolveCrewxApiCredential('pairing')).toEqual({
|
|
56
|
+
token: `crewxrt_pairing_${VALID_HEX}`,
|
|
57
|
+
source: 'file',
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('prefers CREWX_API_TOKEN without inspecting the runtime credential file', () => {
|
|
62
|
+
writeCredential('wi', 0o644);
|
|
63
|
+
process.env['CREWX_API_TOKEN'] = 'env-token';
|
|
64
|
+
|
|
65
|
+
expect(resolveCrewxApiCredential('wi')).toEqual({ token: 'env-token', source: 'env' });
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('fails with MISSING and a non-zero exit code when the runtime file is absent', () => {
|
|
69
|
+
expect(() => resolveCrewxApiCredential('notify')).toThrow(CrewxCredentialError);
|
|
70
|
+
|
|
71
|
+
try {
|
|
72
|
+
resolveCrewxApiCredential('notify');
|
|
73
|
+
throw new Error('expected resolveCrewxApiCredential to throw');
|
|
74
|
+
} catch (error) {
|
|
75
|
+
expect(error).toMatchObject({ code: 'MISSING', exitCode: expect.any(Number) });
|
|
76
|
+
expect((error as Error).message).toContain('notify.token');
|
|
77
|
+
expect((error as CrewxCredentialError).exitCode).toBe(78);
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('fails with a distinct BAD_MODE error when the runtime file is not 0600', () => {
|
|
82
|
+
writeCredential('notify', 0o644);
|
|
83
|
+
|
|
84
|
+
expect(() => resolveCrewxApiCredential('notify')).toThrow(CrewxCredentialError);
|
|
85
|
+
|
|
86
|
+
try {
|
|
87
|
+
resolveCrewxApiCredential('notify');
|
|
88
|
+
throw new Error('expected resolveCrewxApiCredential to throw');
|
|
89
|
+
} catch (error) {
|
|
90
|
+
expect(error).toMatchObject({ code: 'BAD_MODE', exitCode: expect.any(Number) });
|
|
91
|
+
expect((error as Error).message).toContain('0600');
|
|
92
|
+
expect((error as CrewxCredentialError).exitCode).toBe(77);
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('fails with UNREADABLE when the file content does not match the purpose contract', () => {
|
|
97
|
+
writeCredential('chromex-negotiate', 0o600, 'not-a-runtime-token');
|
|
98
|
+
|
|
99
|
+
expect(() => resolveCrewxApiCredential('chromex-negotiate')).toThrow(CrewxCredentialError);
|
|
100
|
+
|
|
101
|
+
try {
|
|
102
|
+
resolveCrewxApiCredential('chromex-negotiate');
|
|
103
|
+
throw new Error('expected resolveCrewxApiCredential to throw');
|
|
104
|
+
} catch (error) {
|
|
105
|
+
expect(error).toMatchObject({ code: 'UNREADABLE', exitCode: expect.any(Number) });
|
|
106
|
+
expect((error as CrewxCredentialError).exitCode).toBe(74);
|
|
107
|
+
expect((error as Error).message).not.toContain('not-a-runtime-token');
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
});
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "CommonJS",
|
|
5
|
+
"moduleResolution": "node",
|
|
6
|
+
"rootDir": ".",
|
|
7
|
+
"outDir": ".",
|
|
8
|
+
"declaration": true,
|
|
9
|
+
"declarationMap": false,
|
|
10
|
+
"sourceMap": false,
|
|
11
|
+
"strict": true,
|
|
12
|
+
"esModuleInterop": true,
|
|
13
|
+
"skipLibCheck": true,
|
|
14
|
+
"forceConsistentCasingInFileNames": true
|
|
15
|
+
},
|
|
16
|
+
"include": ["crewx-api-credential.ts"],
|
|
17
|
+
"exclude": ["node_modules", "tests"]
|
|
18
|
+
}
|