CI (SIAX Cloud) / sonar (pull_request) Skipped
CI (SIAX Cloud) / integration (push) Skipped
CI (SIAX Cloud) / sonar (push) Skipped
masterplan-lock / check (pull_request) Successful in 3s
CI (SIAX Cloud) / security (push) Successful in 15s
Security Scan (SIAX Cloud) / security (push) Successful in 15s
Security Scan (SIAX Cloud) / security (pull_request) Successful in 15s
CI (SIAX Cloud) / security (pull_request) Successful in 1m2s
CI (SIAX Cloud) / integration (pull_request) Successful in 1m11s
masterplan-lock / check (push) Successful in 1m38s
CI (SIAX Cloud) / quality (push) Successful in 1m49s
CI (SIAX Cloud) / quality (pull_request) Successful in 1m50s
DOC0 denies an x-actor-id that the service identity has no actor_grant for, so every upload/structure call from 0pt failed with DOC0_ACCESS_DENIED and became outcome_unknown. The journal and project authorization keep the verified end-user actor; only the DOC0 request acts as the service (DOC0_SERVICE_ACTOR_ID).
107 lines
3.6 KiB
TypeScript
107 lines
3.6 KiB
TypeScript
/**
|
|
* Server-side runtime configuration for the document workbench.
|
|
*
|
|
* Every integration is optional to BOOT and mandatory to USE: a route that
|
|
* needs DOC0/INF0/PAY0 returns a typed "not configured" error instead of
|
|
* fabricating success. URLs must be trusted HTTPS endpoints; tokens are never
|
|
* logged or returned.
|
|
*
|
|
* This module is deliberately framework-neutral and free of product imports so
|
|
* it can be lifted into a shared package without change.
|
|
*/
|
|
|
|
export interface HttpsEndpoint {
|
|
baseUrl: string;
|
|
serviceToken: string;
|
|
}
|
|
|
|
export interface DocumentWorkbenchRuntime {
|
|
doc0?: HttpsEndpoint & { maxUploadBytes: number; actorId?: string };
|
|
inf0?: HttpsEndpoint;
|
|
pay0?: HttpsEndpoint;
|
|
n0d?: HttpsEndpoint;
|
|
st0re?: HttpsEndpoint;
|
|
openfga?: {
|
|
baseUrl: string;
|
|
storeId: string;
|
|
modelId: string;
|
|
serviceToken: string;
|
|
};
|
|
}
|
|
|
|
const DEFAULT_MAX_UPLOAD_BYTES = 25 * 1024 * 1024;
|
|
|
|
function first(...values: Array<string | undefined>): string | undefined {
|
|
for (const value of values) {
|
|
if (typeof value === 'string' && value.trim()) return value.trim();
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
/** Only absolute HTTPS endpoints with no credentials/query/hash are trusted. */
|
|
export function isTrustedHttpsUrl(value: string | undefined): value is string {
|
|
if (!value) return false;
|
|
try {
|
|
const url = new URL(value);
|
|
return url.protocol === 'https:' && !url.username && !url.password && !url.search && !url.hash;
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
function endpoint(
|
|
env: NodeJS.ProcessEnv,
|
|
urlNames: readonly string[],
|
|
tokenNames: readonly string[],
|
|
): HttpsEndpoint | undefined {
|
|
const baseUrl = first(...urlNames.map((name) => env[name]));
|
|
const serviceToken = first(...tokenNames.map((name) => env[name]));
|
|
if (!isTrustedHttpsUrl(baseUrl) || !serviceToken) return undefined;
|
|
return { baseUrl: baseUrl.replace(/\/+$/, ''), serviceToken };
|
|
}
|
|
|
|
function positiveInt(value: string | undefined, fallback: number): number {
|
|
if (!value) return fallback;
|
|
const parsed = Number(value);
|
|
return Number.isSafeInteger(parsed) && parsed > 0 ? parsed : fallback;
|
|
}
|
|
|
|
export function readDocumentWorkbenchRuntime(env: NodeJS.ProcessEnv = process.env): DocumentWorkbenchRuntime {
|
|
const doc0 = endpoint(env, ['DOC0_URL', 'DOC0_API_URL'], ['DOC0_SERVICE_TOKEN', 'DOC0_SVC_PAT']);
|
|
const inf0 = endpoint(env, ['INF0_URL', 'INF0_API_URL'], ['INF0_API_TOKEN', 'INF0_SERVICE_TOKEN']);
|
|
const pay0 = endpoint(env, ['PAY0_URL', 'PAY0_API_URL'], ['PAY0_API_TOKEN', 'PAY0_SERVICE_TOKEN']);
|
|
const n0d = endpoint(env, ['N0D_URL', 'N0D_API_URL'], ['N0D_API_TOKEN', 'N0D_SERVICE_TOKEN']);
|
|
const st0re = endpoint(env, ['ST0RE_URL', 'ST0RE_API_URL'], ['ST0RE_API_TOKEN', 'ST0RE_SERVICE_TOKEN']);
|
|
|
|
const openfgaBase = first(env.OPENFGA_API_URL);
|
|
const openfgaStore = first(env.OPENFGA_STORE_ID);
|
|
const openfgaModel = first(env.OPENFGA_MODEL_ID);
|
|
const openfgaToken = first(env.OPENFGA_AUTH_TOKEN, env.OPENFGA_AUTH_KEY);
|
|
|
|
return {
|
|
...(doc0
|
|
? {
|
|
doc0: {
|
|
...doc0,
|
|
maxUploadBytes: positiveInt(env.OPT_MAX_UPLOAD_BYTES, DEFAULT_MAX_UPLOAD_BYTES),
|
|
...(env.DOC0_SERVICE_ACTOR_ID ? { actorId: env.DOC0_SERVICE_ACTOR_ID.trim() } : {}),
|
|
},
|
|
}
|
|
: {}),
|
|
...(inf0 ? { inf0 } : {}),
|
|
...(pay0 ? { pay0 } : {}),
|
|
...(n0d ? { n0d } : {}),
|
|
...(st0re ? { st0re } : {}),
|
|
...(isTrustedHttpsUrl(openfgaBase) && openfgaStore && openfgaModel && openfgaToken
|
|
? {
|
|
openfga: {
|
|
baseUrl: openfgaBase.replace(/\/+$/, ''),
|
|
storeId: openfgaStore,
|
|
modelId: openfgaModel,
|
|
serviceToken: openfgaToken,
|
|
},
|
|
}
|
|
: {}),
|
|
};
|
|
}
|