1 Commits

Author SHA1 Message Date
57974aeabd fix: persist order outcomes and prevent workflow bypass 2026-09-07 18:47:04 +08:00
10 changed files with 20 additions and 575 deletions

View File

@@ -1,8 +1,8 @@
{
"moduleCode": "order",
"candidateVersion": "1.0.2-rc.3",
"buildRevision": 3,
"buildChecksum": "eef24d3edf9b35c4496e5eb854f46e27d8678c5f484b02a6045e3f47a6385e84",
"schemaInputChecksum": "d0a174052b88d7018d5bd7f175cb030a1c39f1a711d37114518ebd5dd9e8e920",
"branchName": "release/1.0.2-rc.3"
"candidateVersion": "1.0.0-rc.1",
"buildRevision": 1,
"buildChecksum": "5cf032fdac2b0403fbcad8b44b326a29141ba556bbbb9710ac9329b5d7c031dd",
"schemaInputChecksum": "1b3eec01e5b04880166e20e8e6df70b79f227129ea9165e38cfbb925be3c2d23",
"branchName": "release/1.0.0-rc.1"
}

5
bootstrap.js vendored
View File

@@ -11,11 +11,10 @@ function loadGeneratedSaasModule() {
return require('./dist/saas/generated').GeneratedSaasModule;
}
async function start(options = {}) {
const session = await prepareSaasServiceHost({
async function start() {
await prepareSaasServiceHost({
generated: loadGeneratedSaasModule(),
});
if (options.verifySession) await options.verifySession(session);
requireExactReleaseEnvironment();
return Bootstrap.configure({
baseDir: path.join(__dirname, 'dist'),

View File

@@ -10,17 +10,14 @@
"build": "rimraf dist && mwtsc --cleanOutDir",
"publish": "cool saas-model finalize",
"start": "node bootstrap.js",
"start:stable": "node start-stable.js",
"test:stable": "node --test test/start-stable.test.cjs",
"start:local": "cross-env NODE_ENV=local SAAS_RUNTIME_BOOTSTRAP_LOCAL_FALLBACK_ENABLED=true node bootstrap.js",
"verify:packages": "node scripts/verify-local-packages.cjs"
},
"coolSaas": {
"lifecycle": {
"dev": "pnpm run build && pnpm run start:local",
"test": "pnpm run typecheck && pnpm run test:stable",
"build": "pnpm run build",
"artifactDir": "dist"
"test": "pnpm run typecheck",
"build": "pnpm run build"
}
},
"dependencies": {

View File

@@ -20,9 +20,9 @@ export class Orders extends SaasTableModel<OrdersRow> {
static readonly definition = defineSaasTableModel({
"kind": "table",
"moduleCode": "order",
"revision": 3,
"schemaVersion": 3,
"schemaInputChecksum": "d0a174052b88d7018d5bd7f175cb030a1c39f1a711d37114518ebd5dd9e8e920",
"revision": 1,
"schemaVersion": 1,
"schemaInputChecksum": "1b3eec01e5b04880166e20e8e6df70b79f227129ea9165e38cfbb925be3c2d23",
"tableCode": "orders",
"fields": [
{

View File

@@ -4,9 +4,9 @@ import { defineGeneratedSaasModule } from '@cool-midway/module-runtime';
export const GeneratedSaasModule = defineGeneratedSaasModule({
"formatVersion": 2,
"moduleCode": "order",
"revision": 3,
"schemaVersion": 3,
"schemaInputChecksum": "d0a174052b88d7018d5bd7f175cb030a1c39f1a711d37114518ebd5dd9e8e920",
"revision": 1,
"schemaVersion": 1,
"schemaInputChecksum": "1b3eec01e5b04880166e20e8e6df70b79f227129ea9165e38cfbb925be3c2d23",
"runtimeContractChecksum": "3d025d1b634c413449eb91164fb1f859a388d8930d2de112980a606b591befce",
"implementationManifestChecksum": "f25647d2dd90e14d6226683d2ff67ba32132b2dfdeb861015805240a96d4635b"
});

View File

@@ -1,9 +1,9 @@
{
"manifestVersion": 1,
"moduleCode": "order",
"revision": 3,
"revision": 1,
"schemaChecksum": null,
"schemaInputChecksum": "d0a174052b88d7018d5bd7f175cb030a1c39f1a711d37114518ebd5dd9e8e920",
"schemaInputChecksum": "1b3eec01e5b04880166e20e8e6df70b79f227129ea9165e38cfbb925be3c2d23",
"actionManifestChecksum": null,
"runtimeContractChecksum": "3d025d1b634c413449eb91164fb1f859a388d8930d2de112980a606b591befce",
"implementationManifestChecksum": "f25647d2dd90e14d6226683d2ff67ba32132b2dfdeb861015805240a96d4635b",

View File

@@ -1,10 +1,10 @@
{
"formatVersion": 2,
"moduleCode": "order",
"revision": 3,
"revision": 1,
"releaseVersion": null,
"schemaVersion": 3,
"schemaInputChecksum": "d0a174052b88d7018d5bd7f175cb030a1c39f1a711d37114518ebd5dd9e8e920",
"schemaVersion": 1,
"schemaInputChecksum": "1b3eec01e5b04880166e20e8e6df70b79f227129ea9165e38cfbb925be3c2d23",
"schemaChecksum": null,
"actionManifestChecksum": null,
"runtimeContractChecksum": "3d025d1b634c413449eb91164fb1f859a388d8930d2de112980a606b591befce",

View File

@@ -1,186 +0,0 @@
'use strict';
// Starts an already-built, frozen directory. There is intentionally no build,
// codegen, Git checkout, publish, or fallback-to-RC operation in this entrypoint.
const { createHash } = require('node:crypto');
const { readdirSync, lstatSync, readFileSync } = require('node:fs');
const path = require('node:path');
const { spawnSync } = require('node:child_process');
const SHA256 = /^[a-f0-9]{64}$/;
const COMMIT = /^(?:[a-f0-9]{40}|[a-f0-9]{64})$/;
const STABLE = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/;
function ensure(condition, message) {
if (!condition) throw new Error(`Stable startup refused: ${message}`);
}
function canonicalJson(value) {
if (Array.isArray(value)) return `[${value.map(canonicalJson).join(',')}]`;
if (value && typeof value === 'object') return `{${Object.keys(value).sort().map(key => `${JSON.stringify(key)}:${canonicalJson(value[key])}`).join(',')}}`;
return JSON.stringify(value);
}
function candidateLockChecksum(lock) {
ensure(Object.keys(lock || {}).sort().join(',') === 'branchName,buildChecksum,buildRevision,candidateVersion,moduleCode,schemaInputChecksum', 'invalid Candidate lock fields');
return createHash('sha256').update(canonicalJson(lock), 'utf8').digest('hex');
}
function calculateCompiledArtifactDigest(projectDir) {
const root = path.resolve(projectDir, 'dist');
ensure(lstatSync(root, { throwIfNoEntry: false })?.isDirectory(), 'dist does not exist; deploy the frozen build, do not rebuild during startup');
const files = [];
function visit(directory) {
for (const name of readdirSync(directory)) {
const file = path.join(directory, name);
const stat = lstatSync(file);
ensure(!stat.isSymbolicLink(), 'compiled artifact must not contain symbolic links');
if (stat.isDirectory()) visit(file);
else {
ensure(stat.isFile(), 'compiled artifact contains a non-file entry');
files.push([path.relative(root, file).replace(/\\/g, '/'), file]);
}
}
}
visit(root);
ensure(files.length > 0, 'compiled artifact is empty');
// Exact algorithm used by cool saas-model finalize with artifactDir: dist.
const hash = createHash('sha256').update('COOL_SAAS_ARTIFACT_DIRECTORY_V1\0', 'utf8');
for (const [name, file] of files.sort(([left], [right]) => left.localeCompare(right))) {
const content = readFileSync(file);
hash.update(`${Buffer.byteLength(name, 'utf8')}:`, 'utf8');
hash.update(name, 'utf8');
hash.update(`:${content.length}:`, 'utf8');
hash.update(content);
}
return `sha256:${hash.digest('hex')}`;
}
function sourceCommit(projectDir, environment) {
const gitOptions = { cwd: projectDir, encoding: 'utf8', windowsHide: true, timeout: 30_000 };
const root = spawnSync('git', ['rev-parse', '--show-toplevel'], gitOptions);
if (root.status === 0) {
ensure(path.resolve(root.stdout.trim()) === path.resolve(projectDir), 'project must be its own exact Git checkout');
const status = spawnSync('git', ['status', '--porcelain=v1', '--untracked-files=all'], gitOptions);
ensure(status.status === 0 && !status.stdout.trim(), 'Git checkout is dirty');
const commit = spawnSync('git', ['rev-parse', 'HEAD'], gitOptions);
ensure(commit.status === 0 && COMMIT.test(commit.stdout.trim()), 'Git HEAD is unavailable');
ensure(!environment.BUILD_SHA || environment.BUILD_SHA === commit.stdout.trim(), 'BUILD_SHA differs from Git HEAD');
return commit.stdout.trim();
}
ensure(!lstatSync(path.join(projectDir, '.git'), { throwIfNoEntry: false }), 'Git metadata exists but could not be verified');
// Git-free deployment archives must carry the trusted orchestrator's commit.
// This is cross-checked against both the authoritative Frozen Artifact and
// its actual compiled bytes, never accepted as a digest substitute.
ensure(COMMIT.test(environment.BUILD_SHA || ''), 'Git-free deployments require an exact BUILD_SHA');
return environment.BUILD_SHA;
}
async function fetchReleaseEvidence(environment, fetcher = fetch) {
const releaseId = Number(environment.RPC_RELEASE_ID);
ensure(Number.isSafeInteger(releaseId) && releaseId > 0, 'RPC_RELEASE_ID must select a Stable release');
const base = new URL(environment.SAAS_CONTROL_PLANE_URL || environment.COOL_SAAS_CONTROL_PLANE_URL || 'http://127.0.0.1:8080');
const local = ['local', 'test'].includes(environment.NODE_ENV);
ensure(base.protocol === 'https:' || (local && base.protocol === 'http:' && ['127.0.0.1', 'localhost', '[::1]'].includes(base.hostname)), 'use HTTPS (loopback HTTP is local/test only)');
ensure(!base.username && !base.password, 'control-plane URL must not contain credentials');
ensure(String(environment.COOL_SAAS_TOKEN || '').trim(), 'a short-lived CLI/CI Bearer credential is required to verify immutable release evidence');
const url = new URL('/admin/saas/module_release/info', base);
url.searchParams.set('id', String(releaseId));
const response = await fetcher(url, {
method: 'GET', headers: { Authorization: `Bearer ${environment.COOL_SAAS_TOKEN}` },
redirect: 'error', signal: AbortSignal.timeout(10_000)
});
ensure(response.ok, `control plane rejected release evidence (${response.status})`);
const body = await response.json();
ensure(body?.code === 1000 && body.data, 'control plane did not return release evidence');
return body.data;
}
function verifyStableEvidence({ release, lock, generated, digest, commit, releaseId }) {
const build = release?.build;
const artifact = release?.frozenArtifact;
ensure(release?.id === Number(releaseId) && release.releaseType === 'stable' && release.status === 'published' && release.lifecycleVersion === 1, 'target is not a published lifecycle Stable release');
ensure(STABLE.test(release.version || '') && release.serviceVersion === release.version, 'Stable version identity is invalid');
ensure(build && artifact && Number(release.promotedFromReleaseId) === Number(artifact.rcReleaseId) && Number(release.frozenArtifactId) === Number(artifact.id), 'Stable does not reference its source RC frozen artifact');
ensure(Number(build.id) === Number(release.buildSnapshotId) && Number(build.moduleId) === Number(release.moduleId) && Number(artifact.moduleId) === Number(release.moduleId), 'Build/module/artifact association differs');
ensure(Number(build.revision) === Number(lock.buildRevision) && Number(release.buildRevision) === Number(lock.buildRevision) && Number(artifact.buildRevision) === Number(lock.buildRevision), 'Build Revision differs from the Candidate lock');
ensure(SHA256.test(build.buildChecksum || '') && build.buildChecksum === lock.buildChecksum && build.schemaInputChecksum === lock.schemaInputChecksum, 'Build checksums differ from the Candidate lock');
ensure(lock.candidateVersion === `${release.version}-rc.${lock.buildRevision}` && lock.branchName === `release/${lock.candidateVersion}` && artifact.sourceBranch === lock.branchName && artifact.tagName === `v${lock.candidateVersion}`, 'source Candidate branch/version/tag differs');
ensure(candidateLockChecksum(lock) === artifact.generatedLockChecksum, 'generated Candidate lock checksum differs');
ensure(COMMIT.test(artifact.sourceCommit || '') && artifact.sourceCommit === commit, 'source commit differs from the Frozen Artifact');
ensure(/^sha256:[a-f0-9]{64}$/.test(artifact.artifactDigest || ''), 'Frozen Artifact has no compiled digest; old Git-only evidence cannot be upgraded or forged here');
ensure(artifact.artifactDigest === digest, 'compiled artifact digest differs from the Frozen Artifact');
ensure(SHA256.test(artifact.artifactIdentityChecksum || '') && SHA256.test(artifact.implementationManifestChecksum || ''), 'frozen implementation identity is incomplete');
ensure(generated.moduleCode === lock.moduleCode && generated.revision === lock.buildRevision && generated.schemaInputChecksum === lock.schemaInputChecksum, 'compiled generated identity differs from the immutable Build');
ensure(!generated.releaseVersion || generated.releaseVersion === release.version, 'generated runtime embeds a different Release version');
ensure(!generated.implementationManifestChecksum || generated.implementationManifestChecksum === artifact.implementationManifestChecksum, 'compiled implementation checksum differs');
const manifest = release.schemaSnapshot?.actionManifest;
const contracts = release.schemaSnapshot?.runtimeContracts;
ensure(manifest && contracts?.implementationManifestChecksum === artifact.implementationManifestChecksum && manifest.artifactDigest === artifact.artifactDigest, 'Stable release envelope differs from the frozen implementation/artifact');
ensure(generated.runtimeContractChecksum === contracts.runtimeContractChecksum && SHA256.test(contracts.runtimeContractChecksum || ''), 'compiled Runtime contract checksum differs');
ensure(generated.schemaVersion === release.schemaVersion && build.schemaVersion === release.schemaVersion, 'compiled Schema version differs');
return {
SAAS_RUNTIME_MODE: 'production', RPC_REQUIRE_EXACT_RELEASE: 'true',
RPC_MODULE_CODE: lock.moduleCode, RPC_RELEASE_ID: String(release.id),
RPC_RELEASE_VERSION: release.version, RPC_SERVICE_VERSION: release.serviceVersion,
SAAS_BUILD_REVISION: String(build.revision), SAAS_BUILD_CHECKSUM: build.buildChecksum,
SAAS_SCHEMA_INPUT_CHECKSUM: build.schemaInputChecksum,
RPC_MIN_SCHEMA_VERSION: String(release.schemaVersion), RPC_MAX_SCHEMA_VERSION: String(release.schemaVersion),
RPC_SCHEMA_CHECKSUM: release.schemaChecksum,
RPC_ACTION_MANIFEST_CHECKSUM: release.schemaSnapshot.actionManifestChecksum,
RPC_RUNTIME_CONTRACT_CHECKSUM: contracts.runtimeContractChecksum,
RPC_IMPLEMENTATION_MANIFEST_CHECKSUM: artifact.implementationManifestChecksum,
RPC_ARTIFACT_DIGEST: artifact.artifactDigest,
RPC_ARTIFACT_IDENTITY_CHECKSUM: artifact.artifactIdentityChecksum,
BUILD_SHA: artifact.sourceCommit
};
}
function applyExactEnvironment(environment, exact) {
for (const [key, value] of Object.entries(exact)) {
ensure(value != null && String(value).length > 0, `authoritative field ${key} is missing`);
ensure(!environment[key] || environment[key] === String(value), `${key} conflicts with authoritative Stable evidence`);
environment[key] = String(value);
}
}
function verifyBootstrapSession(session, exact) {
ensure(session?.moduleCode === exact.RPC_MODULE_CODE && session.revision === Number(exact.SAAS_BUILD_REVISION), 'bootstrap module/Revision differs');
const mapping = {
releaseId: 'RPC_RELEASE_ID', releaseVersion: 'RPC_RELEASE_VERSION', serviceVersion: 'RPC_SERVICE_VERSION',
runtimeMode: 'SAAS_RUNTIME_MODE', buildRevision: 'SAAS_BUILD_REVISION', buildChecksum: 'SAAS_BUILD_CHECKSUM',
schemaInputChecksum: 'SAAS_SCHEMA_INPUT_CHECKSUM', implementationManifestChecksum: 'RPC_IMPLEMENTATION_MANIFEST_CHECKSUM',
artifactDigest: 'RPC_ARTIFACT_DIGEST', artifactIdentityChecksum: 'RPC_ARTIFACT_IDENTITY_CHECKSUM'
};
for (const [field, key] of Object.entries(mapping)) ensure(String(session.release?.[field]) === exact[key], `authenticated bootstrap ${field} differs from Stable evidence`);
}
async function startStable(options = {}) {
const projectDir = path.resolve(options.projectDir || __dirname);
const environment = options.environment || process.env;
const release = await fetchReleaseEvidence(environment, options.fetch);
const lock = JSON.parse(readFileSync(path.join(projectDir, '.cool-saas-candidate.json'), 'utf8'));
const digest = calculateCompiledArtifactDigest(projectDir);
const commit = sourceCommit(projectDir, environment);
// Do not execute generated JavaScript before its compiled digest is verified.
ensure(digest === release.frozenArtifact?.artifactDigest, 'compiled artifact digest differs from the Frozen Artifact');
const generated = (options.loadGenerated || (() => require(path.join(projectDir, 'dist/saas/generated')).GeneratedSaasModule))();
const exact = verifyStableEvidence({ release, lock, generated, digest, commit, releaseId: environment.RPC_RELEASE_ID });
applyExactEnvironment(environment, exact);
environment.SAAS_CONTROL_PLANE_URL ||= environment.COOL_SAAS_CONTROL_PLANE_URL || 'http://127.0.0.1:8080';
delete environment.COOL_SAAS_TOKEN;
delete environment.NODE_AUTH_TOKEN;
delete environment.npm_config__authToken;
const start = options.start || require(path.join(projectDir, 'bootstrap.js')).start;
return start({ verifySession: session => {
verifyBootstrapSession(session, exact);
ensure(calculateCompiledArtifactDigest(projectDir) === digest, 'compiled bytes changed during bootstrap');
} });
}
if (require.main === module) startStable().catch(error => {
process.stderr.write(`${error?.message || error}\n`);
process.exitCode = 1;
});
module.exports = { startStable, verifyStableEvidence, verifyBootstrapSession, applyExactEnvironment, calculateCompiledArtifactDigest, candidateLockChecksum, fetchReleaseEvidence, sourceCommit };

View File

@@ -1,170 +0,0 @@
'use strict';
// Exercise the actual TypeScript service, with only its injected CRUD/Action
// collaborators stubbed. No application bootstrap, network, database or dist
// writes are needed; generated contracts and decorators remain the real ones.
require('reflect-metadata');
const test = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
const ts = require('typescript');
const { SaasBusinessReject, ModuleRuntimeError } = require('@cool-midway/module-runtime');
const sourceRoot = path.resolve(__dirname, '../src');
const previousTsLoader = require.extensions['.ts'];
let OrdersService;
try {
require.extensions['.ts'] = (module, filename) => {
assert.ok(filename.startsWith(sourceRoot + path.sep), 'only this service source tree is compiled in memory');
const compiled = ts.transpileModule(fs.readFileSync(filename, 'utf8'), {
fileName: filename,
compilerOptions: { target: ts.ScriptTarget.ES2018, module: ts.ModuleKind.CommonJS,
experimentalDecorators: true, emitDecoratorMetadata: true, esModuleInterop: true },
});
module._compile(compiled.outputText, filename);
};
({ OrdersService } = require('../src/service/orders.ts'));
} finally {
if (previousTsLoader) require.extensions['.ts'] = previousTsLoader;
else delete require.extensions['.ts'];
}
function fixture(overrides = {}) {
const state = {
goods: { id: '24', status: 1, price: '12.50', stock: 20 },
order: { id: '27', order_no: 'UNIT-ORDER', goods_id: '24', quantity: 3, amount: '37.50', status: 'confirmed' },
calls: [], ...overrides,
};
const service = new OrdersService();
service.batchInfo = async input => { state.calls.push(['batchInfo', input]); return { list: state.goods ? [state.goods] : [] }; };
service.reserveStock = async input => { state.calls.push(['reserveStock', input]); return { goodsId: input.goodsId, remainingStock: 17 }; };
service.releaseStock = async input => { state.calls.push(['releaseStock', input]); return { goodsId: input.goodsId, currentStock: 20 }; };
service.orders = {
lock: async input => {state.calls.push(['lock',input]);return state.order;},
info: async () => assert.fail('missing-record lookup must not invoke the nonnullable info contract'),
list: async input => { state.calls.push(['list', input]); return state.order ? [state.order] : []; },
add: async input => { state.calls.push(['add', input]); return { id: '27', ...input, ...state.addResult }; },
update: async input => { state.calls.push(['update', input]); return { affected: 1 }; },
};
service.workflow = {start: async input => {state.calls.push(['workflow',input]);return {sagaId:'test-saga'};}};
return { service, state };
}
const createInput = { orderNo: 'UNIT-ORDER', goodsId: 24, quantity: 3 };
const business = code => error => error instanceof SaasBusinessReject && !(error instanceof ModuleRuntimeError) && error.code === code;
const contract = error => error instanceof ModuleRuntimeError && !(error instanceof SaasBusinessReject) &&
error.errorCode === 'MODULE_ACTION_INPUT_INVALID' && error.statusCode === 409;
const noWrites = state => assert.ok(state.calls.every(([name]) => !['reserveStock', 'releaseStock', 'add', 'update'].includes(name)));
test('the existing locked list contract authorizes ID filtering and valid empty-array results', () => {
const lock = JSON.parse(fs.readFileSync(path.join(sourceRoot, 'saas/generated/order/saas-schema.lock.json'), 'utf8'));
const model = lock.models.find(item => item.tableCode === 'orders');
assert.equal(model.fields.find(field => field.code === 'id').filterable, true);
const list = lock.actions.find(item => item.manifestDescriptor.name === 'orders.list').manifestDescriptor;
assert.equal(list.inputSchema.properties.where.properties.id.type, 'string');
assert.equal(list.inputSchema.properties.take.minimum, 1);
assert.equal(list.outputSchema.type, 'array');
assert.ok(list.tables.some(table => table.tableCode === 'orders' && table.select === true && table.fields.includes('id')));
});
test('unlisted goods is an explicit GOODS_UNAVAILABLE business rejection before any write', async () => {
const f = fixture({ goods: { id: '24', status: 0, price: '12.50' } });
await assert.rejects(f.service.createOrder(createInput), business('GOODS_UNAVAILABLE'));
noWrites(f.state);
});
test('missing goods is GOODS_NOT_FOUND in both order creation and details', async t => {
for (const method of ['createOrder', 'detailWithGoods']) await t.test(method, async () => {
const f = fixture({ goods: null });
await assert.rejects(f.service[method](method === 'createOrder' ? createInput : { id: 27 }), business('GOODS_NOT_FOUND'));
noWrites(f.state);
});
});
test('missing order is ORDER_NOT_FOUND in details and cancellation', async t => {
for (const method of ['detailWithGoods', 'cancelOrder']) await t.test(method, async () => {
const f = fixture({ order: null });
await assert.rejects(f.service[method]({ id: 27 }), business('ORDER_NOT_FOUND'));
assert.deepEqual(f.state.calls, method === 'cancelOrder' ? [['lock',{id:'27'}]] : [['list', { where: { id: '27' }, take: 1 }]]);
noWrites(f.state);
});
});
test('malformed goods status and price remain contract errors, not business warnings', async t => {
for (const [label, patch] of [['fractional status', { status: 1.2 }], ['invalid status', { status: 'bad' }],
['invalid price', { price: '12.5oops' }], ['nonfinite price', { price: Infinity }]]) {
await t.test(label, async () => {
const f = fixture(); Object.assign(f.state.goods, patch);
await assert.rejects(f.service.createOrder(createInput), contract);
noWrites(f.state);
});
}
});
test('unsafe stored goods IDs remain contract failures before a cross-module call', async t => {
for (const id of ['0', '-1', '1.5', '9007199254740992', 'bad']) await t.test(id, async () => {
const f = fixture(); f.state.order.goods_id = id;
await assert.rejects(f.service.detailWithGoods({ id: 27 }), contract);
assert.deepEqual(f.state.calls.map(([name]) => name), ['list']);
});
});
test('invalid generated order ID or amount stays a contract error after mocked CRUD', async t => {
for (const [label, addResult] of [['unsafe order ID', { id: '9007199254740992' }], ['nonfinite amount', { amount: 'NaN' }]]) {
await t.test(label, async () => {
const f = fixture({ addResult });
await assert.rejects(f.service.createOrder(createInput), contract);
assert.deepEqual(f.state.calls.map(([name]) => name), ['batchInfo', 'add', 'workflow']);
});
}
});
test('failure to persist workflow propagates to the enclosing transaction', async () => {
const f = fixture();
const downstream = new SaasBusinessReject('INSUFFICIENT_STOCK', '库存不足');
f.service.workflow.start = async () => { throw downstream; };
await assert.rejects(f.service.createOrder(createInput), error => error === downstream);
assert.equal(f.state.calls.some(([name]) => name === 'reserveStock'), false);
});
test('list contract and database failures propagate unchanged instead of becoming ORDER_NOT_FOUND', async t => {
for (const error of [new ModuleRuntimeError('MODULE_ACTION_OUTPUT_INVALID', 'invalid list output', 500),
new ModuleRuntimeError('MODULE_ACTION_FORBIDDEN', 'list access denied', 403), new Error('database unavailable')]) {
await t.test(error.message, async () => {
const f = fixture();
f.service.orders.list = async () => { throw error; };
await assert.rejects(f.service.detailWithGoods({ id: 27 }), observed => observed === error && !(observed instanceof SaasBusinessReject));
noWrites(f.state);
});
}
});
test('creation persists pending intent and starts durable workflow without a synchronous stock write', async () => {
const f = fixture();
assert.deepEqual(await f.service.createOrder(createInput), { id: 27, orderNo: 'UNIT-ORDER', amount: 37.5, status: 'pending_reservation' });
assert.deepEqual(f.state.calls.map(([name]) => name), ['batchInfo', 'add', 'workflow']);
const flow=f.state.calls[2][1];
assert.equal(flow.type,'order.create@1');
assert.deepEqual(flow.steps[0].input,{goodsId:24,quantity:3});
assert.deepEqual(flow.outcomes.completed,{id:'27',status:'confirmed'});
assert.deepEqual(flow.outcomes.failed,{id:'27',status:'rejected'});
});
test('cancellation locks state and persists release workflow; terminal and pending repeats are inert', async () => {
const f = fixture();
assert.deepEqual(await f.service.cancelOrder({ id: 27 }), { id: 27, status: 'cancel_pending', restoredStock: 0 });
assert.deepEqual(f.state.calls.map(([name]) => name), ['lock', 'update', 'workflow']);
const repeated = fixture(); repeated.state.order.status = 'cancelled';
assert.deepEqual(await repeated.service.cancelOrder({ id: 27 }), { id: 27, status: 'cancelled', restoredStock: 0 });
assert.deepEqual(repeated.state.calls.map(([name]) => name), ['lock']);
const pending=fixture(); pending.state.order.status='cancel_pending';
assert.equal((await pending.service.cancelOrder({id:27})).status,'cancel_pending');
assert.deepEqual(pending.state.calls.map(([name])=>name),['lock']);
});
test('legacy or unconfirmed orders cannot manufacture a stock release', async () => {
for (const status of ['created','pending_reservation','rejected','manual_review']) {
const f=fixture();f.state.order.status=status;
await assert.rejects(f.service.cancelOrder({id:27}),business('ORDER_STATE_CONFLICT'));
noWrites(f.state);
}
});

View File

@@ -1,195 +0,0 @@
'use strict';
const assert = require('node:assert/strict');
const test = require('node:test');
const { mkdtempSync, mkdirSync, writeFileSync, readFileSync, rmSync, symlinkSync } = require('node:fs');
const path = require('node:path');
const os = require('node:os');
const { spawnSync } = require('node:child_process');
const { createHash } = require('node:crypto');
const {
startStable, verifyStableEvidence, verifyBootstrapSession, applyExactEnvironment,
calculateCompiledArtifactDigest, candidateLockChecksum, fetchReleaseEvidence, sourceCommit
} = require('../start-stable');
function fixture() {
const lock = { moduleCode: 'demo', candidateVersion: '1.0.1-rc.2', buildRevision: 2, buildChecksum: 'b'.repeat(64), schemaInputChecksum: 'c'.repeat(64), branchName: 'release/1.0.1-rc.2' };
const generated = { moduleCode: 'demo', revision: 2, schemaVersion: 3, schemaInputChecksum: lock.schemaInputChecksum, implementationManifestChecksum: 'd'.repeat(64), runtimeContractChecksum: 'e'.repeat(64) };
const commit = 'a'.repeat(40);
const digest = `sha256:${'f'.repeat(64)}`;
const release = {
id: 42, moduleId: 2, releaseType: 'stable', status: 'published', lifecycleVersion: 1,
version: '1.0.1', serviceVersion: '1.0.1', promotedFromReleaseId: 41, buildSnapshotId: 52,
buildRevision: 2, frozenArtifactId: 62, schemaVersion: 3, schemaChecksum: '3'.repeat(64),
build: { id: 52, moduleId: 2, revision: 2, schemaVersion: 3, buildChecksum: lock.buildChecksum, schemaInputChecksum: lock.schemaInputChecksum },
frozenArtifact: { id: 62, rcReleaseId: 41, moduleId: 2, buildRevision: 2, sourceBranch: lock.branchName, tagName: `v${lock.candidateVersion}`, sourceCommit: commit, generatedLockChecksum: candidateLockChecksum(lock), artifactDigest: digest, artifactIdentityChecksum: '1'.repeat(64), implementationManifestChecksum: generated.implementationManifestChecksum },
schemaSnapshot: { actionManifest: { artifactDigest: digest }, actionManifestChecksum: '2'.repeat(64), runtimeContracts: { implementationManifestChecksum: generated.implementationManifestChecksum, runtimeContractChecksum: generated.runtimeContractChecksum } }
};
return { release, lock, generated, digest, commit, releaseId: 42 };
}
function sessionFor(exact) {
return { moduleCode: exact.RPC_MODULE_CODE, revision: Number(exact.SAAS_BUILD_REVISION), release: {
releaseId: Number(exact.RPC_RELEASE_ID), releaseVersion: exact.RPC_RELEASE_VERSION, serviceVersion: exact.RPC_SERVICE_VERSION,
runtimeMode: exact.SAAS_RUNTIME_MODE, buildRevision: Number(exact.SAAS_BUILD_REVISION), buildChecksum: exact.SAAS_BUILD_CHECKSUM,
schemaInputChecksum: exact.SAAS_SCHEMA_INPUT_CHECKSUM, implementationManifestChecksum: exact.RPC_IMPLEMENTATION_MANIFEST_CHECKSUM,
artifactDigest: exact.RPC_ARTIFACT_DIGEST, artifactIdentityChecksum: exact.RPC_ARTIFACT_IDENTITY_CHECKSUM
} };
}
test('Stable derives its own deployment envelope from the same frozen Candidate and Build', () => {
const input = fixture();
const before = JSON.stringify(input);
const exact = verifyStableEvidence(input);
assert.equal(exact.RPC_RELEASE_ID, '42');
assert.equal(exact.RPC_RELEASE_VERSION, '1.0.1');
assert.equal(exact.SAAS_RUNTIME_MODE, 'production');
assert.equal(exact.SAAS_BUILD_REVISION, '2');
assert.equal(exact.BUILD_SHA, input.commit);
assert.equal(exact.RPC_ARTIFACT_DIGEST, input.digest);
assert.equal(JSON.stringify(input), before, 'must never rewrite the Candidate or frozen evidence');
verifyBootstrapSession(sessionFor(exact), exact);
});
test('fail closed on RC targets, history-only artifacts, or any frozen identity mismatch', () => {
const edits = [
data => { data.release.releaseType = 'rc'; },
data => { data.release.status = 'retired'; },
data => { data.release.version = '1.0.1-rc.2'; },
data => { data.release.frozenArtifact.artifactDigest = null; },
data => { data.release.frozenArtifact.rcReleaseId++; },
data => { data.release.buildSnapshotId++; },
data => { data.release.build.buildChecksum = '9'.repeat(64); },
data => { data.release.buildRevision++; },
data => { data.release.frozenArtifact.generatedLockChecksum = '9'.repeat(64); },
data => { data.release.frozenArtifact.tagName = 'v9.0.0'; },
data => { data.commit = '9'.repeat(40); },
data => { data.digest = `sha256:${'9'.repeat(64)}`; },
data => { data.generated.revision++; },
data => { data.generated.releaseVersion = '1.0.1-rc.2'; },
data => { data.generated.implementationManifestChecksum = '9'.repeat(64); },
data => { data.generated.runtimeContractChecksum = '9'.repeat(64); },
data => { data.generated.schemaVersion++; },
data => { data.release.schemaSnapshot.actionManifest.artifactDigest = `sha256:${'9'.repeat(64)}`; }
];
for (const edit of edits) {
const input = fixture();
edit(input);
assert.throws(() => verifyStableEvidence(input), /Stable startup refused/);
}
});
test('environment and authenticated bootstrap cannot silently replace Stable with RC or a different Build', () => {
const exact = verifyStableEvidence(fixture());
assert.throws(() => applyExactEnvironment({ RPC_RELEASE_ID: '41' }, exact), /conflicts/);
const environment = {};
applyExactEnvironment(environment, exact);
assert.equal(environment.RPC_ARTIFACT_DIGEST, exact.RPC_ARTIFACT_DIGEST);
for (const key of ['releaseId', 'releaseVersion', 'buildChecksum', 'runtimeMode', 'artifactDigest', 'artifactIdentityChecksum']) {
const session = sessionFor(exact);
session.release[key] = 'incorrect';
assert.throws(() => verifyBootstrapSession(session, exact), /differs/);
}
});
test('compiled digest uses the exact finalize byte format and detects changes', t => {
const root = mkdtempSync(path.join(os.tmpdir(), 'stable-digest-test-'));
t.after(() => rmSync(root, { recursive: true, force: true }));
mkdirSync(path.join(root, 'dist/nested'), { recursive: true });
writeFileSync(path.join(root, 'dist/a.js'), 'first');
writeFileSync(path.join(root, 'dist/nested/b.js'), 'second');
const exactHash = createHash('sha256').update('COOL_SAAS_ARTIFACT_DIRECTORY_V1\0', 'utf8');
for (const [name, content] of [['a.js', 'first'], ['nested/b.js', 'second']]) {
exactHash.update(`${Buffer.byteLength(name)}:`).update(name).update(`:${Buffer.byteLength(content)}:`).update(content);
}
assert.equal(calculateCompiledArtifactDigest(root), `sha256:${exactHash.digest('hex')}`);
const oldDigest = calculateCompiledArtifactDigest(root);
writeFileSync(path.join(root, 'dist/a.js'), 'changed');
assert.notEqual(calculateCompiledArtifactDigest(root), oldDigest);
});
test('missing, empty, and linked compiled directories are refused', t => {
const root = mkdtempSync(path.join(os.tmpdir(), 'stable-linked-test-'));
t.after(() => rmSync(root, { recursive: true, force: true }));
assert.throws(() => calculateCompiledArtifactDigest(root), /does not exist/);
mkdirSync(path.join(root, 'dist'));
assert.throws(() => calculateCompiledArtifactDigest(root), /empty/);
mkdirSync(path.join(root, 'outside'));
writeFileSync(path.join(root, 'outside/escaped.js'), 'outside the frozen directory');
symlinkSync(path.join(root, 'outside'), path.join(root, 'dist/linked'), 'junction');
assert.throws(() => calculateCompiledArtifactDigest(root), /symbolic links/);
});
test('control-plane evidence requires a Bearer credential and local HTTP cannot target a remote host', async () => {
const environment = { NODE_ENV: 'local', RPC_RELEASE_ID: '42', SAAS_CONTROL_PLANE_URL: 'http://127.0.0.1:8080', COOL_SAAS_TOKEN: 'test-short-lived-credential' };
const release = fixture().release;
assert.equal(await fetchReleaseEvidence(environment, async (url, options) => {
assert.equal(url.searchParams.get('id'), '42');
assert.equal(options.headers.Authorization, `Bearer ${environment.COOL_SAAS_TOKEN}`);
assert.equal(options.redirect, 'error');
return { ok: true, json: async () => ({ code: 1000, data: release }) };
}), release);
await assert.rejects(fetchReleaseEvidence({ ...environment, COOL_SAAS_TOKEN: '' }), /credential/);
await assert.rejects(fetchReleaseEvidence({ ...environment, SAAS_CONTROL_PLANE_URL: 'http://example.com' }), /HTTPS/);
await assert.rejects(fetchReleaseEvidence(environment, async () => ({ ok: false, status: 401 })), /rejected/);
});
test('actual startup verifies existing bytes, removes CLI secrets, and never invokes a build', async t => {
const root = mkdtempSync(path.join(os.tmpdir(), 'stable-start-test-'));
t.after(() => rmSync(root, { recursive: true, force: true }));
mkdirSync(path.join(root, 'dist'), { recursive: true });
writeFileSync(path.join(root, 'dist/configuration.js'), 'frozen compiled bytes');
const data = fixture();
data.digest = calculateCompiledArtifactDigest(root);
data.release.frozenArtifact.artifactDigest = data.digest;
data.release.schemaSnapshot.actionManifest.artifactDigest = data.digest;
writeFileSync(path.join(root, '.cool-saas-candidate.json'), JSON.stringify(data.lock));
const environment = { NODE_ENV: 'local', RPC_RELEASE_ID: '42', BUILD_SHA: data.commit, COOL_SAAS_TOKEN: 'short-lived-test', NODE_AUTH_TOKEN: 'must-not-inherit' };
let starts = 0;
const options = {
projectDir: root, environment,
fetch: async () => ({ ok: true, json: async () => ({ code: 1000, data: data.release }) }),
loadGenerated: () => data.generated,
start: ({ verifySession }) => {
starts++;
assert.equal(environment.COOL_SAAS_TOKEN, undefined);
assert.equal(environment.NODE_AUTH_TOKEN, undefined);
verifySession(sessionFor(environment));
return 'started';
}
};
assert.equal(await startStable(options), 'started');
assert.equal(starts, 1);
assert.equal(readFileSync(path.join(root, 'dist/configuration.js'), 'utf8'), 'frozen compiled bytes');
writeFileSync(path.join(root, 'dist/configuration.js'), 'tampered');
environment.COOL_SAAS_TOKEN = 'short-lived-test';
options.loadGenerated = () => { throw new Error('must not execute unverified compiled code'); };
await assert.rejects(startStable(options), /compiled artifact digest differs/);
assert.equal(starts, 1);
writeFileSync(path.join(root, 'dist/configuration.js'), 'frozen compiled bytes');
environment.COOL_SAAS_TOKEN = 'short-lived-test';
await assert.rejects(startStable({
...options,
loadGenerated: () => data.generated,
start: ({ verifySession }) => {
writeFileSync(path.join(root, 'dist/configuration.js'), 'changed during bootstrap');
verifySession(sessionFor(environment));
}
}), /changed during bootstrap/);
});
test('dirty Git checkouts cannot be presented as the frozen source commit', t => {
const root = mkdtempSync(path.join(os.tmpdir(), 'stable-git-test-'));
t.after(() => rmSync(root, { recursive: true, force: true }));
const git = (...args) => {
const result = spawnSync('git', args, { cwd: root, encoding: 'utf8', windowsHide: true });
assert.equal(result.status, 0, result.stderr);
return result.stdout.trim();
};
git('init'); git('config', 'user.email', 'test@example.invalid'); git('config', 'user.name', 'Stable Test');
writeFileSync(path.join(root, 'business.js'), 'original');
git('add', '.'); git('commit', '-m', 'frozen');
assert.equal(sourceCommit(root, {}), git('rev-parse', 'HEAD'));
writeFileSync(path.join(root, 'business.js'), 'modified');
assert.throws(() => sourceCommit(root, {}), /dirty/);
});