@bigfootds/bigfootds-service-utils 0.1.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/LICENSE +21 -0
- package/README.md +133 -0
- package/dist/errors.d.ts +72 -0
- package/dist/errors.js +90 -0
- package/dist/express.d.ts +55 -0
- package/dist/express.js +103 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +20 -0
- package/dist/requestIds.d.ts +93 -0
- package/dist/requestIds.js +109 -0
- package/dist/serviceTokens.d.ts +94 -0
- package/dist/serviceTokens.js +116 -0
- package/package.json +62 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 BigfootDS
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
# BigfootDS Service Utils
|
|
2
|
+
|
|
3
|
+
Reusable service-side utilities for BigfootDS microservices.
|
|
4
|
+
|
|
5
|
+
This package helps services apply the same request, error, and internal-caller conventions without copying helper code into each service. It is a package, not a service, and it does not own runtime data.
|
|
6
|
+
|
|
7
|
+
## First-Pass Scope
|
|
8
|
+
|
|
9
|
+
- request ID generation, validation, inbound resolution, active request metadata, and response-header wiring
|
|
10
|
+
- standard JSON error helpers built from the global error catalogue in `@bigfootds/bigfootds-shared-data`
|
|
11
|
+
- framework-neutral bearer service-token verification and service caller policy results
|
|
12
|
+
- thin Express-style adapters for request IDs, service-token verification, and standard error responses
|
|
13
|
+
|
|
14
|
+
Deferred areas include broad validation helpers, audit helpers, Morgan logging helpers, admin/operator bulk helpers, and profanity matching/normalisation helpers.
|
|
15
|
+
|
|
16
|
+
## Public Package Safety
|
|
17
|
+
|
|
18
|
+
This package is publicly published on NPM, so its contents must be safe for public-facing usage.
|
|
19
|
+
|
|
20
|
+
It should contain reusable code, public type definitions, and public convention constants only.
|
|
21
|
+
|
|
22
|
+
Do not put secrets, service-token values, private route policies, private diagnostics, account data, provider payloads, environment-specific URLs, live-ops configuration, audit records, or service-owned runtime data in this package.
|
|
23
|
+
|
|
24
|
+
Service-token helpers accept token values at runtime from the consuming service. Tokens should come from that service's environment or secret manager, never from this package.
|
|
25
|
+
|
|
26
|
+
## Installation
|
|
27
|
+
|
|
28
|
+
```sh
|
|
29
|
+
npm install @bigfootds/bigfootds-service-utils
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Request IDs
|
|
33
|
+
|
|
34
|
+
```ts
|
|
35
|
+
import {
|
|
36
|
+
resolveRequestIdFromHeaders,
|
|
37
|
+
writeRequestIdHeader
|
|
38
|
+
} from "@bigfootds/bigfootds-service-utils";
|
|
39
|
+
|
|
40
|
+
const metadata = resolveRequestIdFromHeaders(request.headers);
|
|
41
|
+
|
|
42
|
+
writeRequestIdHeader(response, metadata.requestId);
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Inbound `x-request-id` values are accepted only when they are safe ASCII, 8-64 characters long, and contain letters, numbers, `.`, `_`, `:`, or `-`. Missing or unsafe values are replaced with a generated UUID.
|
|
46
|
+
|
|
47
|
+
## Standard Errors
|
|
48
|
+
|
|
49
|
+
```ts
|
|
50
|
+
import {
|
|
51
|
+
ERROR_CODES
|
|
52
|
+
} from "@bigfootds/bigfootds-shared-data";
|
|
53
|
+
import {
|
|
54
|
+
createServiceError,
|
|
55
|
+
serializeServiceError
|
|
56
|
+
} from "@bigfootds/bigfootds-service-utils";
|
|
57
|
+
|
|
58
|
+
const error = createServiceError(ERROR_CODES.VALIDATION_FAILED, {
|
|
59
|
+
message: "Display name is required.",
|
|
60
|
+
safeDetails: { field: "displayName" },
|
|
61
|
+
requestId: "req_123456"
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
const { httpStatus, body } = serializeServiceError(error);
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Serialised errors include stable `error.code` values and safe messages. They do not include stack traces, causes, tokens, or private diagnostics.
|
|
68
|
+
|
|
69
|
+
## Service Tokens
|
|
70
|
+
|
|
71
|
+
```ts
|
|
72
|
+
import {
|
|
73
|
+
verifyServiceTokenFromHeaders
|
|
74
|
+
} from "@bigfootds/bigfootds-service-utils";
|
|
75
|
+
|
|
76
|
+
const result = verifyServiceTokenFromHeaders(request.headers, {
|
|
77
|
+
acceptedCallers: [
|
|
78
|
+
{
|
|
79
|
+
serviceId: "ms-auth",
|
|
80
|
+
token: process.env.ACCEPT_MS_AUTH_SERVICE_TOKEN ?? ""
|
|
81
|
+
}
|
|
82
|
+
],
|
|
83
|
+
allowedServiceIds: ["ms-auth"]
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
if (!result.ok) {
|
|
87
|
+
console.log(result.errorCode);
|
|
88
|
+
}
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Callers send tokens with `Authorization: Bearer <token>`. Caller identity comes from the existing `pkg-bigfoot-fetcher` `productName` header. Package-style identities such as `@bigfootds/ms-auth` are normalised to canonical Project IDs such as `ms-auth`.
|
|
92
|
+
|
|
93
|
+
## Express Adapters
|
|
94
|
+
|
|
95
|
+
The Express adapters use structural types, so this package does not require Express as a dependency.
|
|
96
|
+
|
|
97
|
+
```ts
|
|
98
|
+
import {
|
|
99
|
+
requestIdMiddleware,
|
|
100
|
+
serviceTokenMiddleware,
|
|
101
|
+
standardErrorHandler
|
|
102
|
+
} from "@bigfootds/bigfootds-service-utils";
|
|
103
|
+
|
|
104
|
+
app.use(requestIdMiddleware());
|
|
105
|
+
|
|
106
|
+
app.post(
|
|
107
|
+
"/internal/auth-user-deleted",
|
|
108
|
+
serviceTokenMiddleware({
|
|
109
|
+
acceptedCallers: [
|
|
110
|
+
{
|
|
111
|
+
serviceId: "ms-auth",
|
|
112
|
+
token: process.env.ACCEPT_MS_AUTH_SERVICE_TOKEN ?? ""
|
|
113
|
+
}
|
|
114
|
+
],
|
|
115
|
+
allowedServiceIds: ["ms-auth"]
|
|
116
|
+
}),
|
|
117
|
+
controller
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
app.use(standardErrorHandler());
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
## Package Boundary
|
|
124
|
+
|
|
125
|
+
`pkg-service-utils` depends on `@bigfootds/bigfootds-shared-data` for stable Project Definitions, error-code metadata, and shared response data shapes.
|
|
126
|
+
|
|
127
|
+
Do not copy shared data into this package. Do not make `pkg-shared-data` depend on this package.
|
|
128
|
+
|
|
129
|
+
The intended dependency direction is:
|
|
130
|
+
|
|
131
|
+
```text
|
|
132
|
+
microservice -> pkg-service-utils -> pkg-shared-data
|
|
133
|
+
```
|
package/dist/errors.d.ts
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { type GlobalErrorCode, type GlobalErrorHttpStatusCode, type SharedErrorResponse } from "@bigfootds/bigfootds-shared-data";
|
|
2
|
+
/**
|
|
3
|
+
* Options for constructing a standard service error.
|
|
4
|
+
*/
|
|
5
|
+
export interface ServiceErrorOptions {
|
|
6
|
+
/**
|
|
7
|
+
* Safe message override. Defaults to the shared catalogue message.
|
|
8
|
+
*/
|
|
9
|
+
readonly message?: string;
|
|
10
|
+
/**
|
|
11
|
+
* Safe structured context to include in the response payload.
|
|
12
|
+
*/
|
|
13
|
+
readonly safeDetails?: unknown;
|
|
14
|
+
/**
|
|
15
|
+
* Request correlation ID to include in the response payload.
|
|
16
|
+
*/
|
|
17
|
+
readonly requestId?: string;
|
|
18
|
+
/**
|
|
19
|
+
* HTTP status override for this response.
|
|
20
|
+
*/
|
|
21
|
+
readonly httpStatus?: GlobalErrorHttpStatusCode;
|
|
22
|
+
/**
|
|
23
|
+
* Lower-level cause retained on the Error object but never serialised.
|
|
24
|
+
*/
|
|
25
|
+
readonly cause?: unknown;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Options for serialising a standard service error.
|
|
29
|
+
*/
|
|
30
|
+
export interface SerializeServiceErrorOptions {
|
|
31
|
+
/**
|
|
32
|
+
* Request correlation ID to include when the error did not already carry one.
|
|
33
|
+
*/
|
|
34
|
+
readonly requestId?: string;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Status/body pair ready for an HTTP adapter.
|
|
38
|
+
*/
|
|
39
|
+
export interface SerializedServiceError {
|
|
40
|
+
readonly httpStatus: GlobalErrorHttpStatusCode;
|
|
41
|
+
readonly body: SharedErrorResponse;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Error class backed by the shared global error catalogue.
|
|
45
|
+
*/
|
|
46
|
+
export declare class ServiceError extends Error {
|
|
47
|
+
readonly code: GlobalErrorCode;
|
|
48
|
+
readonly httpStatus: GlobalErrorHttpStatusCode;
|
|
49
|
+
readonly requestId?: string;
|
|
50
|
+
readonly safeDetails?: unknown;
|
|
51
|
+
constructor(code: GlobalErrorCode, options?: ServiceErrorOptions);
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Creates a standard service error.
|
|
55
|
+
*/
|
|
56
|
+
export declare function createServiceError(code: GlobalErrorCode, options?: ServiceErrorOptions): ServiceError;
|
|
57
|
+
/**
|
|
58
|
+
* Checks whether a value is a standard service error.
|
|
59
|
+
*/
|
|
60
|
+
export declare function isServiceError(value: unknown): value is ServiceError;
|
|
61
|
+
/**
|
|
62
|
+
* Serialises a standard service error without leaking stack traces or causes.
|
|
63
|
+
*/
|
|
64
|
+
export declare function serializeServiceError(error: ServiceError, options?: SerializeServiceErrorOptions): SerializedServiceError;
|
|
65
|
+
/**
|
|
66
|
+
* Converts unknown thrown values into a standard service error.
|
|
67
|
+
*/
|
|
68
|
+
export declare function normalizeServiceError(error: unknown, options?: ServiceErrorOptions): ServiceError;
|
|
69
|
+
/**
|
|
70
|
+
* Builds a serialised response directly from a global error code.
|
|
71
|
+
*/
|
|
72
|
+
export declare function createErrorResponse(code: GlobalErrorCode, options?: ServiceErrorOptions): SerializedServiceError;
|
package/dist/errors.js
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ServiceError = void 0;
|
|
4
|
+
exports.createServiceError = createServiceError;
|
|
5
|
+
exports.isServiceError = isServiceError;
|
|
6
|
+
exports.serializeServiceError = serializeServiceError;
|
|
7
|
+
exports.normalizeServiceError = normalizeServiceError;
|
|
8
|
+
exports.createErrorResponse = createErrorResponse;
|
|
9
|
+
const bigfootds_shared_data_1 = require("@bigfootds/bigfootds-shared-data");
|
|
10
|
+
/**
|
|
11
|
+
* Error class backed by the shared global error catalogue.
|
|
12
|
+
*/
|
|
13
|
+
class ServiceError extends Error {
|
|
14
|
+
code;
|
|
15
|
+
httpStatus;
|
|
16
|
+
requestId;
|
|
17
|
+
safeDetails;
|
|
18
|
+
constructor(code, options = {}) {
|
|
19
|
+
const definition = (0, bigfootds_shared_data_1.getErrorCodeDefinition)(code);
|
|
20
|
+
const message = options.message ?? definition?.defaultMessage ?? "Something went wrong.";
|
|
21
|
+
const errorOptions = options.cause === undefined ? undefined : { cause: options.cause };
|
|
22
|
+
super(message, errorOptions);
|
|
23
|
+
this.name = "ServiceError";
|
|
24
|
+
this.code = code;
|
|
25
|
+
this.httpStatus = options.httpStatus ?? definition?.defaultHttpStatus ?? 500;
|
|
26
|
+
if (options.requestId !== undefined) {
|
|
27
|
+
this.requestId = options.requestId;
|
|
28
|
+
}
|
|
29
|
+
if (options.safeDetails !== undefined) {
|
|
30
|
+
this.safeDetails = options.safeDetails;
|
|
31
|
+
}
|
|
32
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
33
|
+
if (Error.captureStackTrace) {
|
|
34
|
+
Error.captureStackTrace(this, this.constructor);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
exports.ServiceError = ServiceError;
|
|
39
|
+
/**
|
|
40
|
+
* Creates a standard service error.
|
|
41
|
+
*/
|
|
42
|
+
function createServiceError(code, options = {}) {
|
|
43
|
+
return new ServiceError(code, options);
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Checks whether a value is a standard service error.
|
|
47
|
+
*/
|
|
48
|
+
function isServiceError(value) {
|
|
49
|
+
return value instanceof ServiceError;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Serialises a standard service error without leaking stack traces or causes.
|
|
53
|
+
*/
|
|
54
|
+
function serializeServiceError(error, options = {}) {
|
|
55
|
+
const errorData = {
|
|
56
|
+
code: error.code,
|
|
57
|
+
message: error.message
|
|
58
|
+
};
|
|
59
|
+
const requestId = error.requestId ?? options.requestId;
|
|
60
|
+
if (requestId !== undefined) {
|
|
61
|
+
Object.assign(errorData, { requestId });
|
|
62
|
+
}
|
|
63
|
+
if (error.safeDetails !== undefined) {
|
|
64
|
+
Object.assign(errorData, { details: error.safeDetails });
|
|
65
|
+
}
|
|
66
|
+
return {
|
|
67
|
+
httpStatus: error.httpStatus,
|
|
68
|
+
body: {
|
|
69
|
+
error: errorData
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Converts unknown thrown values into a standard service error.
|
|
75
|
+
*/
|
|
76
|
+
function normalizeServiceError(error, options = {}) {
|
|
77
|
+
if (isServiceError(error)) {
|
|
78
|
+
return error;
|
|
79
|
+
}
|
|
80
|
+
return createServiceError(bigfootds_shared_data_1.ERROR_CODES.INTERNAL_ERROR, {
|
|
81
|
+
...options,
|
|
82
|
+
cause: options.cause ?? error
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Builds a serialised response directly from a global error code.
|
|
87
|
+
*/
|
|
88
|
+
function createErrorResponse(code, options = {}) {
|
|
89
|
+
return serializeServiceError(createServiceError(code, options));
|
|
90
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { type ServiceErrorOptions } from "./errors";
|
|
2
|
+
import { type ActiveRequestMetadata, type HeaderRecord } from "./requestIds";
|
|
3
|
+
import { type ServiceCaller, type ServiceCallerPolicy } from "./serviceTokens";
|
|
4
|
+
/**
|
|
5
|
+
* Request context attached by BigfootDS Express adapters.
|
|
6
|
+
*/
|
|
7
|
+
export interface BigfootDSRequestContext extends ActiveRequestMetadata {
|
|
8
|
+
readonly serviceCaller?: ServiceCaller;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Minimal Express-like request shape used by the adapters.
|
|
12
|
+
*/
|
|
13
|
+
export interface ExpressLikeRequest {
|
|
14
|
+
readonly headers?: HeaderRecord;
|
|
15
|
+
readonly method?: string;
|
|
16
|
+
readonly originalUrl?: string;
|
|
17
|
+
readonly url?: string;
|
|
18
|
+
bigfootds?: BigfootDSRequestContext;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Minimal Express-like response shape used by the adapters.
|
|
22
|
+
*/
|
|
23
|
+
export interface ExpressLikeResponse {
|
|
24
|
+
statusCode?: number;
|
|
25
|
+
readonly headersSent?: boolean;
|
|
26
|
+
set?(name: string, value: string): unknown;
|
|
27
|
+
setHeader?(name: string, value: string): unknown;
|
|
28
|
+
status?(statusCode: number): ExpressLikeResponse;
|
|
29
|
+
json?(body: unknown): unknown;
|
|
30
|
+
send?(body: unknown): unknown;
|
|
31
|
+
end?(body?: unknown): unknown;
|
|
32
|
+
}
|
|
33
|
+
export type ExpressLikeNextFunction = (error?: unknown) => void;
|
|
34
|
+
export type ExpressLikeMiddleware = (request: ExpressLikeRequest, response: ExpressLikeResponse, next: ExpressLikeNextFunction) => void;
|
|
35
|
+
export type ExpressLikeErrorMiddleware = (error: unknown, request: ExpressLikeRequest, response: ExpressLikeResponse, next: ExpressLikeNextFunction) => void;
|
|
36
|
+
export interface RequestIdMiddlewareOptions {
|
|
37
|
+
readonly generateRequestId?: () => string;
|
|
38
|
+
readonly nowMs?: number;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Creates middleware that resolves, stores, and returns the standard request ID.
|
|
42
|
+
*/
|
|
43
|
+
export declare function requestIdMiddleware(options?: RequestIdMiddlewareOptions): ExpressLikeMiddleware;
|
|
44
|
+
/**
|
|
45
|
+
* Creates middleware that verifies bearer service-token callers.
|
|
46
|
+
*/
|
|
47
|
+
export declare function serviceTokenMiddleware(policy: ServiceCallerPolicy): ExpressLikeMiddleware;
|
|
48
|
+
/**
|
|
49
|
+
* Creates Express-style error middleware for standard service responses.
|
|
50
|
+
*/
|
|
51
|
+
export declare function standardErrorHandler(options?: ServiceErrorOptions): ExpressLikeErrorMiddleware;
|
|
52
|
+
/**
|
|
53
|
+
* Sends a standard unauthenticated error response.
|
|
54
|
+
*/
|
|
55
|
+
export declare function sendUnauthorizedResponse(response: ExpressLikeResponse, requestId?: string): void;
|
package/dist/express.js
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.requestIdMiddleware = requestIdMiddleware;
|
|
4
|
+
exports.serviceTokenMiddleware = serviceTokenMiddleware;
|
|
5
|
+
exports.standardErrorHandler = standardErrorHandler;
|
|
6
|
+
exports.sendUnauthorizedResponse = sendUnauthorizedResponse;
|
|
7
|
+
const bigfootds_shared_data_1 = require("@bigfootds/bigfootds-shared-data");
|
|
8
|
+
const errors_1 = require("./errors");
|
|
9
|
+
const requestIds_1 = require("./requestIds");
|
|
10
|
+
const serviceTokens_1 = require("./serviceTokens");
|
|
11
|
+
/**
|
|
12
|
+
* Creates middleware that resolves, stores, and returns the standard request ID.
|
|
13
|
+
*/
|
|
14
|
+
function requestIdMiddleware(options = {}) {
|
|
15
|
+
return (request, response, next) => {
|
|
16
|
+
const metadata = (0, requestIds_1.createActiveRequestMetadata)(request.headers, options);
|
|
17
|
+
request.bigfootds = {
|
|
18
|
+
...request.bigfootds,
|
|
19
|
+
...metadata
|
|
20
|
+
};
|
|
21
|
+
(0, requestIds_1.writeRequestIdHeader)(response, metadata.requestId);
|
|
22
|
+
next();
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Creates middleware that verifies bearer service-token callers.
|
|
27
|
+
*/
|
|
28
|
+
function serviceTokenMiddleware(policy) {
|
|
29
|
+
return (request, response, next) => {
|
|
30
|
+
const context = ensureRequestContext(request, response);
|
|
31
|
+
const result = (0, serviceTokens_1.verifyServiceTokenFromHeaders)(request.headers, policy);
|
|
32
|
+
if (!result.ok) {
|
|
33
|
+
sendSerializedError(response, (0, errors_1.createErrorResponse)(result.errorCode, {
|
|
34
|
+
requestId: context.requestId,
|
|
35
|
+
httpStatus: result.httpStatus
|
|
36
|
+
}));
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
request.bigfootds = {
|
|
40
|
+
...context,
|
|
41
|
+
serviceCaller: result.caller
|
|
42
|
+
};
|
|
43
|
+
next();
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Creates Express-style error middleware for standard service responses.
|
|
48
|
+
*/
|
|
49
|
+
function standardErrorHandler(options = {}) {
|
|
50
|
+
return (error, request, response, next) => {
|
|
51
|
+
if (response.headersSent === true) {
|
|
52
|
+
next(error);
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
const serviceError = (0, errors_1.normalizeServiceError)(error, {
|
|
56
|
+
...options,
|
|
57
|
+
requestId: options.requestId ?? request.bigfootds?.requestId
|
|
58
|
+
});
|
|
59
|
+
sendSerializedError(response, (0, errors_1.serializeServiceError)(serviceError, {
|
|
60
|
+
requestId: options.requestId ?? request.bigfootds?.requestId
|
|
61
|
+
}));
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Sends a standard unauthenticated error response.
|
|
66
|
+
*/
|
|
67
|
+
function sendUnauthorizedResponse(response, requestId) {
|
|
68
|
+
sendSerializedError(response, (0, errors_1.createErrorResponse)(bigfootds_shared_data_1.ERROR_CODES.UNAUTHORIZED, {
|
|
69
|
+
requestId
|
|
70
|
+
}));
|
|
71
|
+
}
|
|
72
|
+
function ensureRequestContext(request, response) {
|
|
73
|
+
if (request.bigfootds !== undefined) {
|
|
74
|
+
return request.bigfootds;
|
|
75
|
+
}
|
|
76
|
+
const metadata = (0, requestIds_1.createActiveRequestMetadata)(request.headers);
|
|
77
|
+
const context = metadata;
|
|
78
|
+
request.bigfootds = context;
|
|
79
|
+
(0, requestIds_1.writeRequestIdHeader)(response, metadata.requestId);
|
|
80
|
+
return context;
|
|
81
|
+
}
|
|
82
|
+
function sendSerializedError(response, serializedError) {
|
|
83
|
+
const statusResponse = response.status?.(serializedError.httpStatus) ?? response;
|
|
84
|
+
if (response.status === undefined) {
|
|
85
|
+
response.statusCode = serializedError.httpStatus;
|
|
86
|
+
}
|
|
87
|
+
if (typeof response.json === "function") {
|
|
88
|
+
response.json(serializedError.body);
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
const body = JSON.stringify(serializedError.body);
|
|
92
|
+
if (typeof response.setHeader === "function") {
|
|
93
|
+
response.setHeader("content-type", "application/json");
|
|
94
|
+
}
|
|
95
|
+
else if (typeof response.set === "function") {
|
|
96
|
+
response.set("content-type", "application/json");
|
|
97
|
+
}
|
|
98
|
+
if (typeof statusResponse.send === "function") {
|
|
99
|
+
statusResponse.send(body);
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
statusResponse.end?.(body);
|
|
103
|
+
}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
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 __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
+
};
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
__exportStar(require("./requestIds"), exports);
|
|
18
|
+
__exportStar(require("./errors"), exports);
|
|
19
|
+
__exportStar(require("./serviceTokens"), exports);
|
|
20
|
+
__exportStar(require("./express"), exports);
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Standard request correlation header used by BigfootDS services.
|
|
3
|
+
*/
|
|
4
|
+
export declare const REQUEST_ID_HEADER_NAME = "x-request-id";
|
|
5
|
+
/**
|
|
6
|
+
* Minimum accepted inbound request ID length.
|
|
7
|
+
*/
|
|
8
|
+
export declare const REQUEST_ID_MIN_LENGTH = 8;
|
|
9
|
+
/**
|
|
10
|
+
* Maximum accepted inbound request ID length.
|
|
11
|
+
*/
|
|
12
|
+
export declare const REQUEST_ID_MAX_LENGTH = 64;
|
|
13
|
+
/**
|
|
14
|
+
* Safe ASCII inbound request ID pattern.
|
|
15
|
+
*/
|
|
16
|
+
export declare const REQUEST_ID_PATTERN: RegExp;
|
|
17
|
+
/**
|
|
18
|
+
* Minimal request-header object shape used by Node and Express.
|
|
19
|
+
*/
|
|
20
|
+
export type HeaderRecord = Readonly<Record<string, string | readonly string[] | undefined>>;
|
|
21
|
+
/**
|
|
22
|
+
* Minimal response-header writer shape used by Node and Express.
|
|
23
|
+
*/
|
|
24
|
+
export interface HeaderWriter {
|
|
25
|
+
set?(name: string, value: string): unknown;
|
|
26
|
+
setHeader?(name: string, value: string): unknown;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Request ID resolution result.
|
|
30
|
+
*/
|
|
31
|
+
export interface RequestIdResolution {
|
|
32
|
+
/**
|
|
33
|
+
* Request ID that should be used for logs, responses, and downstream calls.
|
|
34
|
+
*/
|
|
35
|
+
readonly requestId: string;
|
|
36
|
+
/**
|
|
37
|
+
* Whether the request ID was generated locally instead of accepted from inbound headers.
|
|
38
|
+
*/
|
|
39
|
+
readonly generated: boolean;
|
|
40
|
+
/**
|
|
41
|
+
* Inbound header value, when one was present.
|
|
42
|
+
*/
|
|
43
|
+
readonly inboundRequestId?: string;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Active request metadata shared by middleware and helper code.
|
|
47
|
+
*/
|
|
48
|
+
export interface ActiveRequestMetadata extends RequestIdResolution {
|
|
49
|
+
/**
|
|
50
|
+
* Milliseconds since Unix epoch when request handling started.
|
|
51
|
+
*/
|
|
52
|
+
readonly startedAtMs: number;
|
|
53
|
+
}
|
|
54
|
+
export interface ResolveRequestIdOptions {
|
|
55
|
+
/**
|
|
56
|
+
* Optional generator used by tests or services that need a custom ID source.
|
|
57
|
+
*/
|
|
58
|
+
readonly generateRequestId?: () => string;
|
|
59
|
+
}
|
|
60
|
+
export interface CreateRequestMetadataOptions extends ResolveRequestIdOptions {
|
|
61
|
+
/**
|
|
62
|
+
* Millisecond timestamp to use for the metadata.
|
|
63
|
+
*/
|
|
64
|
+
readonly nowMs?: number;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Generates a new request ID using Node's UUID implementation.
|
|
68
|
+
*/
|
|
69
|
+
export declare function generateRequestId(): string;
|
|
70
|
+
/**
|
|
71
|
+
* Checks whether a raw value is safe to accept as an inbound request ID.
|
|
72
|
+
*/
|
|
73
|
+
export declare function isValidRequestId(value: unknown): value is string;
|
|
74
|
+
/**
|
|
75
|
+
* Reads a header value from a header record using case-insensitive matching.
|
|
76
|
+
*/
|
|
77
|
+
export declare function getHeaderValue(headers: HeaderRecord | undefined, headerName: string): string | undefined;
|
|
78
|
+
/**
|
|
79
|
+
* Resolves an accepted inbound request ID or generates a replacement.
|
|
80
|
+
*/
|
|
81
|
+
export declare function resolveRequestId(inboundRequestId?: unknown, options?: ResolveRequestIdOptions): RequestIdResolution;
|
|
82
|
+
/**
|
|
83
|
+
* Resolves the standard request ID from a header record.
|
|
84
|
+
*/
|
|
85
|
+
export declare function resolveRequestIdFromHeaders(headers: HeaderRecord | undefined, options?: ResolveRequestIdOptions): RequestIdResolution;
|
|
86
|
+
/**
|
|
87
|
+
* Creates active request metadata from inbound headers.
|
|
88
|
+
*/
|
|
89
|
+
export declare function createActiveRequestMetadata(headers: HeaderRecord | undefined, options?: CreateRequestMetadataOptions): ActiveRequestMetadata;
|
|
90
|
+
/**
|
|
91
|
+
* Writes the request ID to a response-like object.
|
|
92
|
+
*/
|
|
93
|
+
export declare function writeRequestIdHeader(response: HeaderWriter, requestId: string, headerName?: string): void;
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.REQUEST_ID_PATTERN = exports.REQUEST_ID_MAX_LENGTH = exports.REQUEST_ID_MIN_LENGTH = exports.REQUEST_ID_HEADER_NAME = void 0;
|
|
4
|
+
exports.generateRequestId = generateRequestId;
|
|
5
|
+
exports.isValidRequestId = isValidRequestId;
|
|
6
|
+
exports.getHeaderValue = getHeaderValue;
|
|
7
|
+
exports.resolveRequestId = resolveRequestId;
|
|
8
|
+
exports.resolveRequestIdFromHeaders = resolveRequestIdFromHeaders;
|
|
9
|
+
exports.createActiveRequestMetadata = createActiveRequestMetadata;
|
|
10
|
+
exports.writeRequestIdHeader = writeRequestIdHeader;
|
|
11
|
+
const node_crypto_1 = require("node:crypto");
|
|
12
|
+
/**
|
|
13
|
+
* Standard request correlation header used by BigfootDS services.
|
|
14
|
+
*/
|
|
15
|
+
exports.REQUEST_ID_HEADER_NAME = "x-request-id";
|
|
16
|
+
/**
|
|
17
|
+
* Minimum accepted inbound request ID length.
|
|
18
|
+
*/
|
|
19
|
+
exports.REQUEST_ID_MIN_LENGTH = 8;
|
|
20
|
+
/**
|
|
21
|
+
* Maximum accepted inbound request ID length.
|
|
22
|
+
*/
|
|
23
|
+
exports.REQUEST_ID_MAX_LENGTH = 64;
|
|
24
|
+
/**
|
|
25
|
+
* Safe ASCII inbound request ID pattern.
|
|
26
|
+
*/
|
|
27
|
+
exports.REQUEST_ID_PATTERN = /^[A-Za-z0-9._:-]{8,64}$/;
|
|
28
|
+
/**
|
|
29
|
+
* Generates a new request ID using Node's UUID implementation.
|
|
30
|
+
*/
|
|
31
|
+
function generateRequestId() {
|
|
32
|
+
return (0, node_crypto_1.randomUUID)();
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Checks whether a raw value is safe to accept as an inbound request ID.
|
|
36
|
+
*/
|
|
37
|
+
function isValidRequestId(value) {
|
|
38
|
+
return typeof value === "string" && exports.REQUEST_ID_PATTERN.test(value);
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Reads a header value from a header record using case-insensitive matching.
|
|
42
|
+
*/
|
|
43
|
+
function getHeaderValue(headers, headerName) {
|
|
44
|
+
if (headers === undefined) {
|
|
45
|
+
return undefined;
|
|
46
|
+
}
|
|
47
|
+
const directValue = readSingleHeaderValue(headers[headerName]);
|
|
48
|
+
if (directValue !== undefined) {
|
|
49
|
+
return directValue;
|
|
50
|
+
}
|
|
51
|
+
const lowerHeaderName = headerName.toLowerCase();
|
|
52
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
53
|
+
if (key.toLowerCase() === lowerHeaderName) {
|
|
54
|
+
return readSingleHeaderValue(value);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return undefined;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Resolves an accepted inbound request ID or generates a replacement.
|
|
61
|
+
*/
|
|
62
|
+
function resolveRequestId(inboundRequestId, options = {}) {
|
|
63
|
+
const headerValue = readSingleHeaderValue(inboundRequestId);
|
|
64
|
+
if (isValidRequestId(headerValue)) {
|
|
65
|
+
return {
|
|
66
|
+
requestId: headerValue,
|
|
67
|
+
generated: false,
|
|
68
|
+
inboundRequestId: headerValue
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
return {
|
|
72
|
+
requestId: options.generateRequestId?.() ?? generateRequestId(),
|
|
73
|
+
generated: true,
|
|
74
|
+
inboundRequestId: headerValue
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Resolves the standard request ID from a header record.
|
|
79
|
+
*/
|
|
80
|
+
function resolveRequestIdFromHeaders(headers, options = {}) {
|
|
81
|
+
return resolveRequestId(getHeaderValue(headers, exports.REQUEST_ID_HEADER_NAME), options);
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Creates active request metadata from inbound headers.
|
|
85
|
+
*/
|
|
86
|
+
function createActiveRequestMetadata(headers, options = {}) {
|
|
87
|
+
return {
|
|
88
|
+
...resolveRequestIdFromHeaders(headers, options),
|
|
89
|
+
startedAtMs: options.nowMs ?? Date.now()
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Writes the request ID to a response-like object.
|
|
94
|
+
*/
|
|
95
|
+
function writeRequestIdHeader(response, requestId, headerName = exports.REQUEST_ID_HEADER_NAME) {
|
|
96
|
+
if (typeof response.setHeader === "function") {
|
|
97
|
+
response.setHeader(headerName, requestId);
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
if (typeof response.set === "function") {
|
|
101
|
+
response.set(headerName, requestId);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
function readSingleHeaderValue(value) {
|
|
105
|
+
if (Array.isArray(value)) {
|
|
106
|
+
return typeof value[0] === "string" ? value[0] : undefined;
|
|
107
|
+
}
|
|
108
|
+
return typeof value === "string" ? value : undefined;
|
|
109
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { type GlobalErrorCode } from "@bigfootds/bigfootds-shared-data";
|
|
2
|
+
import { type HeaderRecord } from "./requestIds";
|
|
3
|
+
/**
|
|
4
|
+
* Header populated by `pkg-bigfoot-fetcher` with the caller's package/product name.
|
|
5
|
+
*/
|
|
6
|
+
export declare const SERVICE_CALLER_HEADER_NAME = "productName";
|
|
7
|
+
/**
|
|
8
|
+
* Standard HTTP authorization header name.
|
|
9
|
+
*/
|
|
10
|
+
export declare const AUTHORIZATION_HEADER_NAME = "authorization";
|
|
11
|
+
/**
|
|
12
|
+
* Expected authorization scheme for service tokens.
|
|
13
|
+
*/
|
|
14
|
+
export declare const SERVICE_TOKEN_AUTHORIZATION_SCHEME = "Bearer";
|
|
15
|
+
/**
|
|
16
|
+
* Token accepted for one calling service.
|
|
17
|
+
*/
|
|
18
|
+
export interface ServiceTokenCredential {
|
|
19
|
+
/**
|
|
20
|
+
* Canonical caller service ID, such as `ms-auth`.
|
|
21
|
+
*/
|
|
22
|
+
readonly serviceId: string;
|
|
23
|
+
/**
|
|
24
|
+
* Secret token expected for this caller.
|
|
25
|
+
*/
|
|
26
|
+
readonly token: string;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Route-level service caller policy.
|
|
30
|
+
*/
|
|
31
|
+
export interface ServiceCallerPolicy {
|
|
32
|
+
/**
|
|
33
|
+
* Callers and tokens this target route can authenticate.
|
|
34
|
+
*/
|
|
35
|
+
readonly acceptedCallers: readonly ServiceTokenCredential[];
|
|
36
|
+
/**
|
|
37
|
+
* Optional route-level allowlist. When omitted, all accepted callers are allowed.
|
|
38
|
+
*/
|
|
39
|
+
readonly allowedServiceIds?: readonly string[];
|
|
40
|
+
/**
|
|
41
|
+
* Header used for caller identity. Defaults to `productName`.
|
|
42
|
+
*/
|
|
43
|
+
readonly serviceIdentityHeaderName?: string;
|
|
44
|
+
/**
|
|
45
|
+
* Header used for bearer token authentication. Defaults to `authorization`.
|
|
46
|
+
*/
|
|
47
|
+
readonly authorizationHeaderName?: string;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Authenticated service caller identity.
|
|
51
|
+
*/
|
|
52
|
+
export interface ServiceCaller {
|
|
53
|
+
/**
|
|
54
|
+
* Canonical service ID after normalisation.
|
|
55
|
+
*/
|
|
56
|
+
readonly serviceId: string;
|
|
57
|
+
/**
|
|
58
|
+
* Raw service identity value declared by the caller.
|
|
59
|
+
*/
|
|
60
|
+
readonly declaredServiceId: string;
|
|
61
|
+
}
|
|
62
|
+
export type ServiceTokenFailureReason = "missing_service_identity" | "missing_authorization" | "malformed_authorization" | "unknown_service_identity" | "token_mismatch" | "caller_not_allowed";
|
|
63
|
+
export interface ServiceTokenVerificationSuccess {
|
|
64
|
+
readonly ok: true;
|
|
65
|
+
readonly caller: ServiceCaller;
|
|
66
|
+
}
|
|
67
|
+
export interface ServiceTokenVerificationFailure {
|
|
68
|
+
readonly ok: false;
|
|
69
|
+
readonly reason: ServiceTokenFailureReason;
|
|
70
|
+
readonly errorCode: GlobalErrorCode;
|
|
71
|
+
readonly httpStatus: 401 | 403;
|
|
72
|
+
}
|
|
73
|
+
export type ServiceTokenVerificationResult = ServiceTokenVerificationSuccess | ServiceTokenVerificationFailure;
|
|
74
|
+
export interface VerifyServiceTokenInput {
|
|
75
|
+
readonly authorizationHeader?: string;
|
|
76
|
+
readonly serviceIdentityHeader?: string;
|
|
77
|
+
readonly policy: ServiceCallerPolicy;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Verifies a bearer service token and route-level service caller policy.
|
|
81
|
+
*/
|
|
82
|
+
export declare function verifyServiceToken(input: VerifyServiceTokenInput): ServiceTokenVerificationResult;
|
|
83
|
+
/**
|
|
84
|
+
* Verifies a service token from request headers.
|
|
85
|
+
*/
|
|
86
|
+
export declare function verifyServiceTokenFromHeaders(headers: HeaderRecord | undefined, policy: ServiceCallerPolicy): ServiceTokenVerificationResult;
|
|
87
|
+
/**
|
|
88
|
+
* Extracts a bearer token from an Authorization header.
|
|
89
|
+
*/
|
|
90
|
+
export declare function extractBearerToken(authorizationHeader?: string): string | undefined;
|
|
91
|
+
/**
|
|
92
|
+
* Normalises package-style caller names into canonical BigfootDS project IDs.
|
|
93
|
+
*/
|
|
94
|
+
export declare function normalizeServiceIdentity(serviceIdentity: string): string;
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.SERVICE_TOKEN_AUTHORIZATION_SCHEME = exports.AUTHORIZATION_HEADER_NAME = exports.SERVICE_CALLER_HEADER_NAME = void 0;
|
|
4
|
+
exports.verifyServiceToken = verifyServiceToken;
|
|
5
|
+
exports.verifyServiceTokenFromHeaders = verifyServiceTokenFromHeaders;
|
|
6
|
+
exports.extractBearerToken = extractBearerToken;
|
|
7
|
+
exports.normalizeServiceIdentity = normalizeServiceIdentity;
|
|
8
|
+
const node_crypto_1 = require("node:crypto");
|
|
9
|
+
const bigfootds_shared_data_1 = require("@bigfootds/bigfootds-shared-data");
|
|
10
|
+
const requestIds_1 = require("./requestIds");
|
|
11
|
+
/**
|
|
12
|
+
* Header populated by `pkg-bigfoot-fetcher` with the caller's package/product name.
|
|
13
|
+
*/
|
|
14
|
+
exports.SERVICE_CALLER_HEADER_NAME = "productName";
|
|
15
|
+
/**
|
|
16
|
+
* Standard HTTP authorization header name.
|
|
17
|
+
*/
|
|
18
|
+
exports.AUTHORIZATION_HEADER_NAME = "authorization";
|
|
19
|
+
/**
|
|
20
|
+
* Expected authorization scheme for service tokens.
|
|
21
|
+
*/
|
|
22
|
+
exports.SERVICE_TOKEN_AUTHORIZATION_SCHEME = "Bearer";
|
|
23
|
+
/**
|
|
24
|
+
* Verifies a bearer service token and route-level service caller policy.
|
|
25
|
+
*/
|
|
26
|
+
function verifyServiceToken(input) {
|
|
27
|
+
const declaredServiceId = input.serviceIdentityHeader?.trim();
|
|
28
|
+
if (declaredServiceId === undefined || declaredServiceId === "") {
|
|
29
|
+
return unauthenticated("missing_service_identity");
|
|
30
|
+
}
|
|
31
|
+
const bearerToken = extractBearerToken(input.authorizationHeader);
|
|
32
|
+
if (bearerToken === undefined) {
|
|
33
|
+
return unauthenticated(input.authorizationHeader === undefined ? "missing_authorization" : "malformed_authorization");
|
|
34
|
+
}
|
|
35
|
+
const serviceId = normalizeServiceIdentity(declaredServiceId);
|
|
36
|
+
const credential = input.policy.acceptedCallers.find((candidate) => normalizeServiceIdentity(candidate.serviceId) === serviceId);
|
|
37
|
+
if (credential === undefined) {
|
|
38
|
+
return unauthenticated("unknown_service_identity");
|
|
39
|
+
}
|
|
40
|
+
if (!tokensMatch(bearerToken, credential.token)) {
|
|
41
|
+
return unauthenticated("token_mismatch");
|
|
42
|
+
}
|
|
43
|
+
if (!isCallerAllowed(serviceId, input.policy.allowedServiceIds)) {
|
|
44
|
+
return {
|
|
45
|
+
ok: false,
|
|
46
|
+
reason: "caller_not_allowed",
|
|
47
|
+
errorCode: bigfootds_shared_data_1.ERROR_CODES.SERVICE_FORBIDDEN,
|
|
48
|
+
httpStatus: 403
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
return {
|
|
52
|
+
ok: true,
|
|
53
|
+
caller: {
|
|
54
|
+
serviceId,
|
|
55
|
+
declaredServiceId
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Verifies a service token from request headers.
|
|
61
|
+
*/
|
|
62
|
+
function verifyServiceTokenFromHeaders(headers, policy) {
|
|
63
|
+
return verifyServiceToken({
|
|
64
|
+
authorizationHeader: (0, requestIds_1.getHeaderValue)(headers, policy.authorizationHeaderName ?? exports.AUTHORIZATION_HEADER_NAME),
|
|
65
|
+
serviceIdentityHeader: (0, requestIds_1.getHeaderValue)(headers, policy.serviceIdentityHeaderName ?? exports.SERVICE_CALLER_HEADER_NAME),
|
|
66
|
+
policy
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Extracts a bearer token from an Authorization header.
|
|
71
|
+
*/
|
|
72
|
+
function extractBearerToken(authorizationHeader) {
|
|
73
|
+
if (authorizationHeader === undefined) {
|
|
74
|
+
return undefined;
|
|
75
|
+
}
|
|
76
|
+
const [scheme, token, extra] = authorizationHeader.trim().split(/\s+/);
|
|
77
|
+
if (extra !== undefined ||
|
|
78
|
+
scheme?.toLowerCase() !== exports.SERVICE_TOKEN_AUTHORIZATION_SCHEME.toLowerCase() ||
|
|
79
|
+
token === undefined ||
|
|
80
|
+
token === "") {
|
|
81
|
+
return undefined;
|
|
82
|
+
}
|
|
83
|
+
return token;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Normalises package-style caller names into canonical BigfootDS project IDs.
|
|
87
|
+
*/
|
|
88
|
+
function normalizeServiceIdentity(serviceIdentity) {
|
|
89
|
+
const trimmed = serviceIdentity.trim().toLowerCase();
|
|
90
|
+
return trimmed.startsWith("@bigfootds/") ? trimmed.slice("@bigfootds/".length) : trimmed;
|
|
91
|
+
}
|
|
92
|
+
function unauthenticated(reason) {
|
|
93
|
+
const errorCode = reason === "missing_service_identity" || reason === "missing_authorization"
|
|
94
|
+
? bigfootds_shared_data_1.ERROR_CODES.SERVICE_TOKEN_MISSING
|
|
95
|
+
: bigfootds_shared_data_1.ERROR_CODES.SERVICE_TOKEN_INVALID;
|
|
96
|
+
return {
|
|
97
|
+
ok: false,
|
|
98
|
+
reason,
|
|
99
|
+
errorCode,
|
|
100
|
+
httpStatus: 401
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
function isCallerAllowed(serviceId, allowedServiceIds) {
|
|
104
|
+
if (allowedServiceIds === undefined) {
|
|
105
|
+
return true;
|
|
106
|
+
}
|
|
107
|
+
return allowedServiceIds.map(normalizeServiceIdentity).includes(serviceId);
|
|
108
|
+
}
|
|
109
|
+
function tokensMatch(actualToken, expectedToken) {
|
|
110
|
+
const actual = Buffer.from(actualToken);
|
|
111
|
+
const expected = Buffer.from(expectedToken);
|
|
112
|
+
if (actual.length !== expected.length) {
|
|
113
|
+
return false;
|
|
114
|
+
}
|
|
115
|
+
return (0, node_crypto_1.timingSafeEqual)(actual, expected);
|
|
116
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@bigfootds/bigfootds-service-utils",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Reusable service-side utilities for BigfootDS microservices.",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"files": [
|
|
8
|
+
"dist"
|
|
9
|
+
],
|
|
10
|
+
"exports": {
|
|
11
|
+
".": {
|
|
12
|
+
"types": "./dist/index.d.ts",
|
|
13
|
+
"require": "./dist/index.js",
|
|
14
|
+
"default": "./dist/index.js"
|
|
15
|
+
},
|
|
16
|
+
"./requestIds": {
|
|
17
|
+
"types": "./dist/requestIds.d.ts",
|
|
18
|
+
"require": "./dist/requestIds.js",
|
|
19
|
+
"default": "./dist/requestIds.js"
|
|
20
|
+
},
|
|
21
|
+
"./errors": {
|
|
22
|
+
"types": "./dist/errors.d.ts",
|
|
23
|
+
"require": "./dist/errors.js",
|
|
24
|
+
"default": "./dist/errors.js"
|
|
25
|
+
},
|
|
26
|
+
"./serviceTokens": {
|
|
27
|
+
"types": "./dist/serviceTokens.d.ts",
|
|
28
|
+
"require": "./dist/serviceTokens.js",
|
|
29
|
+
"default": "./dist/serviceTokens.js"
|
|
30
|
+
},
|
|
31
|
+
"./express": {
|
|
32
|
+
"types": "./dist/express.d.ts",
|
|
33
|
+
"require": "./dist/express.js",
|
|
34
|
+
"default": "./dist/express.js"
|
|
35
|
+
},
|
|
36
|
+
"./package.json": "./package.json"
|
|
37
|
+
},
|
|
38
|
+
"scripts": {
|
|
39
|
+
"prebuild": "node -e \"require('node:fs').rmSync('dist', { recursive: true, force: true })\"",
|
|
40
|
+
"build": "tsc -p tsconfig.json",
|
|
41
|
+
"prepack": "npm run build",
|
|
42
|
+
"test": "npm run build && node --test"
|
|
43
|
+
},
|
|
44
|
+
"repository": {
|
|
45
|
+
"type": "git",
|
|
46
|
+
"url": "git+https://github.com/BigfootDS/pkg-service-utils.git"
|
|
47
|
+
},
|
|
48
|
+
"keywords": [],
|
|
49
|
+
"author": "Alex Stormwood <alex.stormwood@bigfootds.com>",
|
|
50
|
+
"license": "MIT",
|
|
51
|
+
"bugs": {
|
|
52
|
+
"url": "https://github.com/BigfootDS/pkg-service-utils/issues"
|
|
53
|
+
},
|
|
54
|
+
"homepage": "https://github.com/BigfootDS/pkg-service-utils#readme",
|
|
55
|
+
"dependencies": {
|
|
56
|
+
"@bigfootds/bigfootds-shared-data": "^2.1.0"
|
|
57
|
+
},
|
|
58
|
+
"devDependencies": {
|
|
59
|
+
"@types/node": "^25.9.2",
|
|
60
|
+
"typescript": "^6.0.3"
|
|
61
|
+
}
|
|
62
|
+
}
|