@agiledigital/pingone-aic-script-tester 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-APACHE +201 -0
- package/LICENSE-MIT +21 -0
- package/README.md +274 -0
- package/dist/bin/fetch-jar.d.ts +2 -0
- package/dist/bin/fetch-jar.js +13 -0
- package/dist/bin/pull-profile.d.ts +2 -0
- package/dist/bin/pull-profile.js +40 -0
- package/dist/bin/show-log.d.ts +2 -0
- package/dist/bin/show-log.js +31 -0
- package/dist/classes/.complete +12 -0
- package/dist/classes/AmClassShutter.class +0 -0
- package/dist/classes/AmContextFactory$ObservedContext.class +0 -0
- package/dist/classes/AmContextFactory.class +0 -0
- package/dist/classes/AmScopes.class +0 -0
- package/dist/classes/HostOps.class +0 -0
- package/dist/classes/JsValues.class +0 -0
- package/dist/classes/Json$Parser.class +0 -0
- package/dist/classes/Json.class +0 -0
- package/dist/classes/LanguageVersions.class +0 -0
- package/dist/classes/Runner.class +0 -0
- package/dist/classes/ScriptContextScope.class +0 -0
- package/dist/src/aic/callbacks.d.ts +35 -0
- package/dist/src/aic/callbacks.js +130 -0
- package/dist/src/aic/conform.d.ts +90 -0
- package/dist/src/aic/conform.js +193 -0
- package/dist/src/aic/constants.d.ts +13 -0
- package/dist/src/aic/constants.js +13 -0
- package/dist/src/aic/diff.d.ts +25 -0
- package/dist/src/aic/diff.js +201 -0
- package/dist/src/aic/emit-journey.d.ts +64 -0
- package/dist/src/aic/emit-journey.js +254 -0
- package/dist/src/aic/emit-result.d.ts +10 -0
- package/dist/src/aic/emit-result.js +39 -0
- package/dist/src/aic/emit-session.d.ts +26 -0
- package/dist/src/aic/emit-session.js +132 -0
- package/dist/src/aic/emit-subject.d.ts +40 -0
- package/dist/src/aic/emit-subject.js +210 -0
- package/dist/src/aic/file-lease.d.ts +35 -0
- package/dist/src/aic/file-lease.js +543 -0
- package/dist/src/aic/http.d.ts +21 -0
- package/dist/src/aic/http.js +127 -0
- package/dist/src/aic/idm.d.ts +10 -0
- package/dist/src/aic/idm.js +112 -0
- package/dist/src/aic/index.d.ts +35 -0
- package/dist/src/aic/index.js +21 -0
- package/dist/src/aic/js.d.ts +8 -0
- package/dist/src/aic/js.js +11 -0
- package/dist/src/aic/lease-identity.d.ts +38 -0
- package/dist/src/aic/lease-identity.js +72 -0
- package/dist/src/aic/lease-lock.d.ts +45 -0
- package/dist/src/aic/lease-lock.js +173 -0
- package/dist/src/aic/managed.d.ts +28 -0
- package/dist/src/aic/managed.js +199 -0
- package/dist/src/aic/provider.d.ts +156 -0
- package/dist/src/aic/provider.js +416 -0
- package/dist/src/aic/record.d.ts +28 -0
- package/dist/src/aic/record.js +183 -0
- package/dist/src/aic/resource-snapshot.d.ts +12 -0
- package/dist/src/aic/resource-snapshot.js +116 -0
- package/dist/src/aic/run.d.ts +102 -0
- package/dist/src/aic/run.js +450 -0
- package/dist/src/aic/tenant.d.ts +107 -0
- package/dist/src/aic/tenant.js +307 -0
- package/dist/src/aic/trace.d.ts +25 -0
- package/dist/src/aic/trace.js +56 -0
- package/dist/src/aic/txid.d.ts +11 -0
- package/dist/src/aic/txid.js +25 -0
- package/dist/src/aic/unsupported.d.ts +15 -0
- package/dist/src/aic/unsupported.js +60 -0
- package/dist/src/bindings/allowlist.d.ts +42 -0
- package/dist/src/bindings/allowlist.js +62 -0
- package/dist/src/bindings/harvest.d.ts +7 -0
- package/dist/src/bindings/harvest.js +137 -0
- package/dist/src/bindings/index.d.ts +4 -0
- package/dist/src/bindings/index.js +3 -0
- package/dist/src/bindings/preamble.d.ts +23 -0
- package/dist/src/bindings/preamble.js +36 -0
- package/dist/src/bindings/run.d.ts +33 -0
- package/dist/src/bindings/run.js +55 -0
- package/dist/src/case/equal.d.ts +2 -0
- package/dist/src/case/equal.js +40 -0
- package/dist/src/case/index.d.ts +5 -0
- package/dist/src/case/index.js +4 -0
- package/dist/src/case/portable.d.ts +10 -0
- package/dist/src/case/portable.js +28 -0
- package/dist/src/case/session.d.ts +34 -0
- package/dist/src/case/session.js +66 -0
- package/dist/src/case/state.d.ts +4 -0
- package/dist/src/case/state.js +47 -0
- package/dist/src/case/types.d.ts +266 -0
- package/dist/src/case/types.js +106 -0
- package/dist/src/case/util.d.ts +7 -0
- package/dist/src/case/util.js +115 -0
- package/dist/src/case/validate.d.ts +8 -0
- package/dist/src/case/validate.js +463 -0
- package/dist/src/case/verdict.d.ts +6 -0
- package/dist/src/case/verdict.js +810 -0
- package/dist/src/diagnostics.d.ts +7 -0
- package/dist/src/diagnostics.js +5 -0
- package/dist/src/emit-dts.d.ts +2 -0
- package/dist/src/emit-dts.js +82 -0
- package/dist/src/emit-js.d.ts +3 -0
- package/dist/src/emit-js.js +149 -0
- package/dist/src/harness/failures.d.ts +26 -0
- package/dist/src/harness/failures.js +113 -0
- package/dist/src/harness/idm.d.ts +12 -0
- package/dist/src/harness/idm.js +44 -0
- package/dist/src/harness/index.d.ts +11 -0
- package/dist/src/harness/index.js +6 -0
- package/dist/src/harness/lease.d.ts +136 -0
- package/dist/src/harness/lease.js +286 -0
- package/dist/src/harness/residue.d.ts +23 -0
- package/dist/src/harness/residue.js +73 -0
- package/dist/src/harness/show-log.d.ts +43 -0
- package/dist/src/harness/show-log.js +253 -0
- package/dist/src/harness/spec.d.ts +67 -0
- package/dist/src/harness/spec.js +189 -0
- package/dist/src/harness/step.d.ts +80 -0
- package/dist/src/harness/step.js +97 -0
- package/dist/src/harness/types.d.ts +100 -0
- package/dist/src/harness/types.js +1 -0
- package/dist/src/harness/vitest.d.ts +52 -0
- package/dist/src/harness/vitest.js +155 -0
- package/dist/src/idents.d.ts +4 -0
- package/dist/src/idents.js +42 -0
- package/dist/src/jvm.d.ts +124 -0
- package/dist/src/jvm.js +432 -0
- package/dist/src/load.d.ts +2 -0
- package/dist/src/load.js +104 -0
- package/dist/src/paths.d.ts +36 -0
- package/dist/src/paths.js +50 -0
- package/dist/src/profile/index.d.ts +5 -0
- package/dist/src/profile/index.js +3 -0
- package/dist/src/profile/normalise.d.ts +16 -0
- package/dist/src/profile/normalise.js +106 -0
- package/dist/src/profile/pull.d.ts +22 -0
- package/dist/src/profile/pull.js +36 -0
- package/dist/src/profile/seed.d.ts +11 -0
- package/dist/src/profile/seed.js +28 -0
- package/dist/src/profile/store.d.ts +13 -0
- package/dist/src/profile/store.js +60 -0
- package/dist/src/profile/types.d.ts +45 -0
- package/dist/src/profile/types.js +1 -0
- package/dist/src/profile/validate.d.ts +38 -0
- package/dist/src/profile/validate.js +129 -0
- package/dist/src/project.d.ts +37 -0
- package/dist/src/project.js +93 -0
- package/dist/src/protocol.d.ts +62 -0
- package/dist/src/protocol.js +68 -0
- package/dist/src/provider-module.d.ts +11 -0
- package/dist/src/provider-module.js +46 -0
- package/dist/src/runner.d.ts +39 -0
- package/dist/src/runner.js +337 -0
- package/dist/src/schema.d.ts +48 -0
- package/dist/src/schema.js +93 -0
- package/generated/scripted-decision-mocks.cjs +722 -0
- package/generated/scripted-decision-mocks.d.ts +626 -0
- package/generated/scripted-decision-next.json +3640 -0
- package/java/AmClassShutter.java +98 -0
- package/java/AmContextFactory.java +69 -0
- package/java/AmScopes.java +49 -0
- package/java/HostOps.java +180 -0
- package/java/JsValues.java +39 -0
- package/java/Json.java +441 -0
- package/java/LanguageVersions.java +55 -0
- package/java/Runner.java +289 -0
- package/java/ScriptContextScope.java +147 -0
- package/package.json +88 -0
- package/src/bindings/rhino/runtime.cjs +3021 -0
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tenant-backed `IdmHandle` for replaying harness checks and cleanup. It keeps
|
|
3
|
+
* the tenant's materialized record shape intact; the local mock's projection
|
|
4
|
+
* is deliberately not reproduced on this lane.
|
|
5
|
+
*/
|
|
6
|
+
import type { JsonValue } from "../case/types.ts";
|
|
7
|
+
import type { IdmHandle } from "../harness/types.ts";
|
|
8
|
+
import { type AicIo, type TenantSession } from "./tenant.ts";
|
|
9
|
+
export declare function tenantIdmHandle(io: AicIo, session: TenantSession): IdmHandle;
|
|
10
|
+
export declare function encodeQueryFilter(filter: Readonly<Record<string, JsonValue>>): string;
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { isPlainObject } from "../case/util.js";
|
|
2
|
+
import { splitResource } from "../harness/idm.js";
|
|
3
|
+
import { AicLaneError, amRequest } from "./tenant.js";
|
|
4
|
+
export function tenantIdmHandle(io, session) {
|
|
5
|
+
return {
|
|
6
|
+
async read(resource) {
|
|
7
|
+
const response = await amRequest(io, session, {
|
|
8
|
+
method: "GET",
|
|
9
|
+
path: recordPath(resource),
|
|
10
|
+
});
|
|
11
|
+
if (response.status === 404) {
|
|
12
|
+
return null;
|
|
13
|
+
}
|
|
14
|
+
if (response.status !== 200) {
|
|
15
|
+
throw statusError("read managed record", response.status, "200 or 404");
|
|
16
|
+
}
|
|
17
|
+
if (!isManagedRecord(response.body)) {
|
|
18
|
+
throw new AicLaneError("read managed record returned no materialized record");
|
|
19
|
+
}
|
|
20
|
+
return response.body;
|
|
21
|
+
},
|
|
22
|
+
async query(type, filter) {
|
|
23
|
+
const queryFilter = encodeQueryFilter(filter);
|
|
24
|
+
const response = await amRequest(io, session, {
|
|
25
|
+
method: "GET",
|
|
26
|
+
path: `${collectionPath(type)}?_queryFilter=${encodeURIComponent(queryFilter)}`,
|
|
27
|
+
});
|
|
28
|
+
if (response.status !== 200 || !isPlainObject(response.body)) {
|
|
29
|
+
throw statusError("query managed records", response.status, "200");
|
|
30
|
+
}
|
|
31
|
+
const result = response.body.result;
|
|
32
|
+
const resultCount = response.body.resultCount;
|
|
33
|
+
if (!Array.isArray(result) ||
|
|
34
|
+
!result.every(isManagedRecord) ||
|
|
35
|
+
typeof resultCount !== "number") {
|
|
36
|
+
throw new AicLaneError("query managed records returned an invalid result/resultCount shape");
|
|
37
|
+
}
|
|
38
|
+
return result;
|
|
39
|
+
},
|
|
40
|
+
async delete(resource) {
|
|
41
|
+
const response = await amRequest(io, session, {
|
|
42
|
+
method: "DELETE",
|
|
43
|
+
path: recordPath(resource),
|
|
44
|
+
});
|
|
45
|
+
if (response.status === 404) {
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
if (response.status !== 200) {
|
|
49
|
+
throw statusError("delete managed record", response.status, "200 or 404");
|
|
50
|
+
}
|
|
51
|
+
if (!isManagedRecord(response.body)) {
|
|
52
|
+
throw new AicLaneError("delete managed record returned no deleted record");
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
export function encodeQueryFilter(filter) {
|
|
58
|
+
const entries = Object.entries(filter);
|
|
59
|
+
if (entries.length === 0) {
|
|
60
|
+
return "true";
|
|
61
|
+
}
|
|
62
|
+
return entries
|
|
63
|
+
.map(([field, value]) => `${encodeField(field)} eq ${encodeFilterValue(field, value)}`)
|
|
64
|
+
.join(" and ");
|
|
65
|
+
}
|
|
66
|
+
function recordPath(resource) {
|
|
67
|
+
const [type, id] = splitResource(resource);
|
|
68
|
+
return `${collectionPath(type)}/${encodeURIComponent(id)}`;
|
|
69
|
+
}
|
|
70
|
+
function collectionPath(type) {
|
|
71
|
+
const match = /^managed\/([^/]+)$/.exec(type);
|
|
72
|
+
if (match === null || match[1] === undefined) {
|
|
73
|
+
throw new AicLaneError(`managed type ${JSON.stringify(type)} must have the form managed/<type>`);
|
|
74
|
+
}
|
|
75
|
+
return `/openidm/managed/${encodeURIComponent(match[1])}`;
|
|
76
|
+
}
|
|
77
|
+
function encodeField(field) {
|
|
78
|
+
if (!/^\/?[A-Za-z0-9_$.-]+(?:\/[A-Za-z0-9_$.-]+)*$/.test(field)) {
|
|
79
|
+
throw new AicLaneError(`managed query field ${JSON.stringify(field)} cannot be encoded safely`);
|
|
80
|
+
}
|
|
81
|
+
return field;
|
|
82
|
+
}
|
|
83
|
+
function encodeFilterValue(field, value) {
|
|
84
|
+
if (typeof value === "string") {
|
|
85
|
+
if (value.includes('"') || hasControlCharacter(value)) {
|
|
86
|
+
throw unsupportedFilterValue(field, "strings containing quotes or control characters");
|
|
87
|
+
}
|
|
88
|
+
return `"${value}"`;
|
|
89
|
+
}
|
|
90
|
+
if (typeof value === "boolean") {
|
|
91
|
+
return String(value);
|
|
92
|
+
}
|
|
93
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
94
|
+
return String(value);
|
|
95
|
+
}
|
|
96
|
+
if (value === null) {
|
|
97
|
+
throw unsupportedFilterValue(field, "null (CREST has no null literal)");
|
|
98
|
+
}
|
|
99
|
+
throw unsupportedFilterValue(field, "arrays and objects");
|
|
100
|
+
}
|
|
101
|
+
function hasControlCharacter(value) {
|
|
102
|
+
return [...value].some((character) => character.charCodeAt(0) < 0x20);
|
|
103
|
+
}
|
|
104
|
+
function isManagedRecord(value) {
|
|
105
|
+
return isPlainObject(value) && typeof value._id === "string";
|
|
106
|
+
}
|
|
107
|
+
function unsupportedFilterValue(field, detail) {
|
|
108
|
+
return new AicLaneError(`managed query field ${JSON.stringify(field)} uses an unencodable value: ${detail}`);
|
|
109
|
+
}
|
|
110
|
+
function statusError(operation, status, expected) {
|
|
111
|
+
return new AicLaneError(`${operation} returned HTTP ${status}, expected ${expected}`, { status });
|
|
112
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AIC wrapper-journey lane. The same case definitions (`src/case`) drive this
|
|
3
|
+
* lane and the local Rhino lane; both are judged by `judge()` in verdict.ts.
|
|
4
|
+
*/
|
|
5
|
+
export { HARNESS_CALLBACK_ID, FAILURE_NODE_ID, SUCCESS_NODE_ID } from "./constants.ts";
|
|
6
|
+
export { parseAuthenticateCallbacks } from "./callbacks.ts";
|
|
7
|
+
export type { AuthenticateCallbacks } from "./callbacks.ts";
|
|
8
|
+
export { chainFromRunResult, conform, conformChain } from "./conform.ts";
|
|
9
|
+
export type { AicChainRunner, ChainConformanceInput, ChainConformanceReport, ChainPassReport, ConformanceInput, ConformanceReport, LaneResult, LaneRunner, LocalChainResult, } from "./conform.ts";
|
|
10
|
+
export { diffRecordedEffects, judgeBoth } from "./diff.ts";
|
|
11
|
+
export type { EffectsComparison, EffectsDisagreement, ObservationGap, } from "./diff.ts";
|
|
12
|
+
export { emitLeasedJourney, emitWrapperJourney, subjectOutcomes } from "./emit-journey.ts";
|
|
13
|
+
export type { EmitJourneyOptions, LeasedJourneyOptions, NodeResource, ScriptResource, WrapperInvoke, WrapperJourney, } from "./emit-journey.ts";
|
|
14
|
+
export { AIC_LEASE_NAMESPACE, createLeaseIdentity, normalizedLeaseOutcomes, sha256, uuidV5, } from "./lease-identity.ts";
|
|
15
|
+
export type { LeaseIdentity, LeaseIdentityOptions, LeaseResourceIds, } from "./lease-identity.ts";
|
|
16
|
+
export { emitResultScript } from "./emit-result.ts";
|
|
17
|
+
export { emitLeasedSessionJourney } from "./emit-session.ts";
|
|
18
|
+
export { instrumentSubject } from "./emit-subject.ts";
|
|
19
|
+
export type { InstrumentedSubject } from "./emit-subject.ts";
|
|
20
|
+
export type { ManagedFixture } from "./managed.ts";
|
|
21
|
+
export { AicFileLease } from "./file-lease.ts";
|
|
22
|
+
export type { AicFileLeaseOptions, AicLeaseRunRequest } from "./file-lease.ts";
|
|
23
|
+
export { acquireLeaseLock, addJournalFixture, addJournalResources, leaseStatePaths, newLeaseJournal, readLeaseJournal, removeJournalFixture, removeLeaseJournal, writeLeaseJournal, } from "./lease-lock.ts";
|
|
24
|
+
export type { LeaseJournal, LeaseLock, LeaseStatePaths, } from "./lease-lock.ts";
|
|
25
|
+
export { confirmResourceSnapshot, nodeRequestProjection, resourceRequestProjection, } from "./resource-snapshot.ts";
|
|
26
|
+
export type { AicResourceKind } from "./resource-snapshot.ts";
|
|
27
|
+
export { assembleEffects, classifyFinal, parseSubjectDump } from "./record.ts";
|
|
28
|
+
export type { SubjectDump } from "./record.ts";
|
|
29
|
+
export { runAicChain, runAicLane, validateAicRun } from "./run.ts";
|
|
30
|
+
export type { AicReply, AicRunValidation, RunAicOptions } from "./run.ts";
|
|
31
|
+
export { AicLaneError, AM_CONFIG_API_VERSION, amConfigHeaders, aicCliProvider, connectTenant, defaultAicIo, resolveTenantProvider, } from "./tenant.ts";
|
|
32
|
+
export type { AicIo, ConnectOptions, TenantSession } from "./tenant.ts";
|
|
33
|
+
export { checkTenantDescription, configuredTenantProvider, DEFAULT_SERVICE_ACCOUNT_SCOPE, logKeysReader, providerFromEnv, serviceAccountProvider, setTenantProvider, TENANT_ENV, tokenCallbackProvider, TenantProviderError, } from "./provider.ts";
|
|
34
|
+
export type { HttpSend, LogKeys, LogReader, ServiceAccountOptions, TenantDescription, TenantProvider, TokenCallbackOptions, TokenRequest, } from "./provider.ts";
|
|
35
|
+
export { aicUnsupportedReason } from "./unsupported.ts";
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AIC wrapper-journey lane. The same case definitions (`src/case`) drive this
|
|
3
|
+
* lane and the local Rhino lane; both are judged by `judge()` in verdict.ts.
|
|
4
|
+
*/
|
|
5
|
+
export { HARNESS_CALLBACK_ID, FAILURE_NODE_ID, SUCCESS_NODE_ID } from "./constants.js";
|
|
6
|
+
export { parseAuthenticateCallbacks } from "./callbacks.js";
|
|
7
|
+
export { chainFromRunResult, conform, conformChain } from "./conform.js";
|
|
8
|
+
export { diffRecordedEffects, judgeBoth } from "./diff.js";
|
|
9
|
+
export { emitLeasedJourney, emitWrapperJourney, subjectOutcomes } from "./emit-journey.js";
|
|
10
|
+
export { AIC_LEASE_NAMESPACE, createLeaseIdentity, normalizedLeaseOutcomes, sha256, uuidV5, } from "./lease-identity.js";
|
|
11
|
+
export { emitResultScript } from "./emit-result.js";
|
|
12
|
+
export { emitLeasedSessionJourney } from "./emit-session.js";
|
|
13
|
+
export { instrumentSubject } from "./emit-subject.js";
|
|
14
|
+
export { AicFileLease } from "./file-lease.js";
|
|
15
|
+
export { acquireLeaseLock, addJournalFixture, addJournalResources, leaseStatePaths, newLeaseJournal, readLeaseJournal, removeJournalFixture, removeLeaseJournal, writeLeaseJournal, } from "./lease-lock.js";
|
|
16
|
+
export { confirmResourceSnapshot, nodeRequestProjection, resourceRequestProjection, } from "./resource-snapshot.js";
|
|
17
|
+
export { assembleEffects, classifyFinal, parseSubjectDump } from "./record.js";
|
|
18
|
+
export { runAicChain, runAicLane, validateAicRun } from "./run.js";
|
|
19
|
+
export { AicLaneError, AM_CONFIG_API_VERSION, amConfigHeaders, aicCliProvider, connectTenant, defaultAicIo, resolveTenantProvider, } from "./tenant.js";
|
|
20
|
+
export { checkTenantDescription, configuredTenantProvider, DEFAULT_SERVICE_ACCOUNT_SCOPE, logKeysReader, providerFromEnv, serviceAccountProvider, setTenantProvider, TENANT_ENV, tokenCallbackProvider, TenantProviderError, } from "./provider.js";
|
|
21
|
+
export { aicUnsupportedReason } from "./unsupported.js";
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { JsonValue } from "../case/types.ts";
|
|
2
|
+
/**
|
|
3
|
+
* AM-safe expression that reconstructs `value` via `JSON.parse`.
|
|
4
|
+
* Avoids object shorthand, destructuring, and other Rhino parse errors
|
|
5
|
+
* that a hand-emitted object literal would have to dodge per property.
|
|
6
|
+
*/
|
|
7
|
+
export declare function jsonParseCall(value: JsonValue): string;
|
|
8
|
+
export declare function jsStringLiteral(value: string): string;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AM-safe expression that reconstructs `value` via `JSON.parse`.
|
|
3
|
+
* Avoids object shorthand, destructuring, and other Rhino parse errors
|
|
4
|
+
* that a hand-emitted object literal would have to dodge per property.
|
|
5
|
+
*/
|
|
6
|
+
export function jsonParseCall(value) {
|
|
7
|
+
return `JSON.parse(${JSON.stringify(JSON.stringify(value))})`;
|
|
8
|
+
}
|
|
9
|
+
export function jsStringLiteral(value) {
|
|
10
|
+
return JSON.stringify(value);
|
|
11
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A private UUID namespace for rhino-local AIC lease resources.
|
|
3
|
+
*
|
|
4
|
+
* Changing it would orphan residue addressed by an earlier harness version.
|
|
5
|
+
*/
|
|
6
|
+
export declare const AIC_LEASE_NAMESPACE = "b42f3f57-ec8e-5e9f-9a2b-59cc9e9d45d0";
|
|
7
|
+
export interface LeaseResourceIds {
|
|
8
|
+
subjectScript: string;
|
|
9
|
+
subjectNode: string;
|
|
10
|
+
resultScripts: Readonly<Record<string, string>>;
|
|
11
|
+
resultNodes: Readonly<Record<string, string>>;
|
|
12
|
+
sessionTree: string;
|
|
13
|
+
sessionScript: string;
|
|
14
|
+
sessionNode: string;
|
|
15
|
+
}
|
|
16
|
+
export interface LeaseIdentity {
|
|
17
|
+
id: string;
|
|
18
|
+
idHash: string;
|
|
19
|
+
treeName: string;
|
|
20
|
+
snapshotKey: string;
|
|
21
|
+
outcomes: readonly string[];
|
|
22
|
+
ids: LeaseResourceIds;
|
|
23
|
+
authorDigest: string;
|
|
24
|
+
structuralDigest: string;
|
|
25
|
+
ownerToken: string;
|
|
26
|
+
marker: string;
|
|
27
|
+
}
|
|
28
|
+
export interface LeaseIdentityOptions {
|
|
29
|
+
id: string;
|
|
30
|
+
source: string;
|
|
31
|
+
outcomes: readonly string[];
|
|
32
|
+
ownerToken?: string;
|
|
33
|
+
}
|
|
34
|
+
export declare function createLeaseIdentity(options: LeaseIdentityOptions): LeaseIdentity;
|
|
35
|
+
export declare function normalizedLeaseOutcomes(outcomes: readonly string[]): string[];
|
|
36
|
+
export declare function sha256(value: string): string;
|
|
37
|
+
/** RFC 4122 UUIDv5 without adding a dependency. */
|
|
38
|
+
export declare function uuidV5(name: string): string;
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/** Stable resource addressing and per-open ownership for the AIC file lease. */
|
|
2
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
3
|
+
/**
|
|
4
|
+
* A private UUID namespace for rhino-local AIC lease resources.
|
|
5
|
+
*
|
|
6
|
+
* Changing it would orphan residue addressed by an earlier harness version.
|
|
7
|
+
*/
|
|
8
|
+
export const AIC_LEASE_NAMESPACE = "b42f3f57-ec8e-5e9f-9a2b-59cc9e9d45d0";
|
|
9
|
+
export function createLeaseIdentity(options) {
|
|
10
|
+
const id = options.id.trim();
|
|
11
|
+
if (id.length === 0) {
|
|
12
|
+
throw new Error("rhino-local: aic.id must not be empty");
|
|
13
|
+
}
|
|
14
|
+
const outcomes = normalizedLeaseOutcomes(options.outcomes);
|
|
15
|
+
const idHash = sha256(id).slice(0, 20);
|
|
16
|
+
const treeName = `rl-aic-${idHash}`;
|
|
17
|
+
const resultScripts = {};
|
|
18
|
+
const resultNodes = {};
|
|
19
|
+
for (const outcome of outcomes) {
|
|
20
|
+
resultScripts[outcome] = uuidV5(`${id}:result-script:${outcome}`);
|
|
21
|
+
resultNodes[outcome] = uuidV5(`${id}:result-node:${outcome}`);
|
|
22
|
+
}
|
|
23
|
+
const authorDigest = sha256(options.source);
|
|
24
|
+
const structuralDigest = sha256(JSON.stringify({ schema: 1, id, outcomes }));
|
|
25
|
+
const ownerToken = options.ownerToken ?? randomUUID();
|
|
26
|
+
const marker = [
|
|
27
|
+
"rhino-local:v1",
|
|
28
|
+
idHash,
|
|
29
|
+
ownerToken,
|
|
30
|
+
authorDigest,
|
|
31
|
+
structuralDigest,
|
|
32
|
+
].join(":");
|
|
33
|
+
return {
|
|
34
|
+
id,
|
|
35
|
+
idHash,
|
|
36
|
+
treeName,
|
|
37
|
+
snapshotKey: `__rhino_local_snapshot_${idHash}`,
|
|
38
|
+
outcomes,
|
|
39
|
+
ids: {
|
|
40
|
+
subjectScript: uuidV5(`${id}:subject-script`),
|
|
41
|
+
subjectNode: uuidV5(`${id}:subject-node`),
|
|
42
|
+
resultScripts,
|
|
43
|
+
resultNodes,
|
|
44
|
+
sessionTree: `rl-aic-${sha256(`${id}:session-tree`).slice(0, 20)}`,
|
|
45
|
+
sessionScript: uuidV5(`${id}:session-script`),
|
|
46
|
+
sessionNode: uuidV5(`${id}:session-node`),
|
|
47
|
+
},
|
|
48
|
+
authorDigest,
|
|
49
|
+
structuralDigest,
|
|
50
|
+
ownerToken,
|
|
51
|
+
marker,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
export function normalizedLeaseOutcomes(outcomes) {
|
|
55
|
+
return [...new Set(["true", "false", ...outcomes])];
|
|
56
|
+
}
|
|
57
|
+
export function sha256(value) {
|
|
58
|
+
return createHash("sha256").update(value, "utf8").digest("hex");
|
|
59
|
+
}
|
|
60
|
+
/** RFC 4122 UUIDv5 without adding a dependency. */
|
|
61
|
+
export function uuidV5(name) {
|
|
62
|
+
const namespace = Buffer.from(AIC_LEASE_NAMESPACE.replace(/-/g, ""), "hex");
|
|
63
|
+
const bytes = createHash("sha1")
|
|
64
|
+
.update(namespace)
|
|
65
|
+
.update(name, "utf8")
|
|
66
|
+
.digest()
|
|
67
|
+
.subarray(0, 16);
|
|
68
|
+
bytes[6] = ((bytes[6] ?? 0) & 0x0f) | 0x50;
|
|
69
|
+
bytes[8] = ((bytes[8] ?? 0) & 0x3f) | 0x80;
|
|
70
|
+
const hex = bytes.toString("hex");
|
|
71
|
+
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
|
|
72
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { LeaseIdentity } from "./lease-identity.ts";
|
|
2
|
+
import type { CreatedResource } from "./run.ts";
|
|
3
|
+
export interface LeaseJournal {
|
|
4
|
+
version: 1;
|
|
5
|
+
tenantHash: string;
|
|
6
|
+
aicId: string;
|
|
7
|
+
realm: string;
|
|
8
|
+
treeName: string;
|
|
9
|
+
ownerToken: string;
|
|
10
|
+
resources: CreatedResource[];
|
|
11
|
+
managedFixtures: Array<{
|
|
12
|
+
type: string;
|
|
13
|
+
id: string;
|
|
14
|
+
}>;
|
|
15
|
+
}
|
|
16
|
+
export interface LeaseStatePaths {
|
|
17
|
+
root: string;
|
|
18
|
+
lockDir: string;
|
|
19
|
+
ownerPath: string;
|
|
20
|
+
journalPath: string;
|
|
21
|
+
tenantHash: string;
|
|
22
|
+
}
|
|
23
|
+
export interface LeaseLock {
|
|
24
|
+
paths: LeaseStatePaths;
|
|
25
|
+
release(): Promise<void>;
|
|
26
|
+
}
|
|
27
|
+
export declare function leaseStatePaths(baseUrl: string, identity: LeaseIdentity, root?: string): LeaseStatePaths;
|
|
28
|
+
export declare function acquireLeaseLock(paths: LeaseStatePaths, identity: LeaseIdentity, options?: {
|
|
29
|
+
timeoutMs?: number;
|
|
30
|
+
processExists?: (pid: number) => boolean;
|
|
31
|
+
pollMs?: number;
|
|
32
|
+
}): Promise<LeaseLock>;
|
|
33
|
+
export declare function newLeaseJournal(paths: LeaseStatePaths, identity: LeaseIdentity, realm: string, resources: readonly CreatedResource[]): LeaseJournal;
|
|
34
|
+
export declare function readLeaseJournal(path: string): Promise<LeaseJournal | undefined>;
|
|
35
|
+
export declare function writeLeaseJournal(path: string, journal: LeaseJournal): Promise<void>;
|
|
36
|
+
export declare function removeLeaseJournal(path: string): Promise<void>;
|
|
37
|
+
export declare function addJournalFixture(path: string, fixture: {
|
|
38
|
+
type: string;
|
|
39
|
+
id: string;
|
|
40
|
+
}): Promise<void>;
|
|
41
|
+
export declare function addJournalResources(path: string, resources: readonly CreatedResource[]): Promise<void>;
|
|
42
|
+
export declare function removeJournalFixture(path: string, fixture: {
|
|
43
|
+
type: string;
|
|
44
|
+
id: string;
|
|
45
|
+
}): Promise<void>;
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
/** Local process lock and identifier-only crash journal for an AIC file lease. */
|
|
2
|
+
import { mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { dirname, join } from "node:path";
|
|
5
|
+
import { randomUUID } from "node:crypto";
|
|
6
|
+
import { sha256 } from "./lease-identity.js";
|
|
7
|
+
import { AicLaneError } from "./tenant.js";
|
|
8
|
+
export function leaseStatePaths(baseUrl, identity, root = join(tmpdir(), "rhino-local-aic-leases")) {
|
|
9
|
+
const tenantHash = sha256(baseUrl).slice(0, 20);
|
|
10
|
+
const key = `${tenantHash}-${identity.idHash}`;
|
|
11
|
+
const lockDir = join(root, `${key}.lock`);
|
|
12
|
+
return {
|
|
13
|
+
root,
|
|
14
|
+
lockDir,
|
|
15
|
+
ownerPath: join(lockDir, "owner.json"),
|
|
16
|
+
journalPath: join(root, `${key}.journal.json`),
|
|
17
|
+
tenantHash,
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
export async function acquireLeaseLock(paths, identity, options = {}) {
|
|
21
|
+
await mkdir(paths.root, { recursive: true, mode: 0o700 });
|
|
22
|
+
const deadline = Date.now() + (options.timeoutMs ?? 120_000);
|
|
23
|
+
const isAlive = options.processExists ?? processExists;
|
|
24
|
+
for (;;) {
|
|
25
|
+
try {
|
|
26
|
+
await mkdir(paths.lockDir, { mode: 0o700 });
|
|
27
|
+
break;
|
|
28
|
+
}
|
|
29
|
+
catch (error) {
|
|
30
|
+
if (!hasCode(error, "EEXIST")) {
|
|
31
|
+
throw error;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
const owner = await readJson(paths.ownerPath);
|
|
35
|
+
if (owner !== undefined && !isAlive(owner.pid)) {
|
|
36
|
+
await rm(paths.lockDir, { recursive: true, force: true });
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
if (Date.now() >= deadline) {
|
|
40
|
+
throw new AicLaneError(`timed out waiting for AIC file lease ${JSON.stringify(identity.id)}${owner === undefined ? "" : ` held by pid ${owner.pid}`}`);
|
|
41
|
+
}
|
|
42
|
+
await delay(options.pollMs ?? 100);
|
|
43
|
+
}
|
|
44
|
+
const owner = {
|
|
45
|
+
pid: process.pid,
|
|
46
|
+
ownerToken: identity.ownerToken,
|
|
47
|
+
aicId: identity.id,
|
|
48
|
+
};
|
|
49
|
+
try {
|
|
50
|
+
await writeFile(paths.ownerPath, JSON.stringify(owner), {
|
|
51
|
+
encoding: "utf8",
|
|
52
|
+
mode: 0o600,
|
|
53
|
+
flag: "wx",
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
catch (error) {
|
|
57
|
+
await rm(paths.lockDir, { recursive: true, force: true });
|
|
58
|
+
throw error;
|
|
59
|
+
}
|
|
60
|
+
let released = false;
|
|
61
|
+
return {
|
|
62
|
+
paths,
|
|
63
|
+
async release() {
|
|
64
|
+
if (released) {
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
const current = await readJson(paths.ownerPath);
|
|
68
|
+
if (current?.ownerToken === identity.ownerToken) {
|
|
69
|
+
await rm(paths.lockDir, { recursive: true, force: true });
|
|
70
|
+
}
|
|
71
|
+
released = true;
|
|
72
|
+
},
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
export function newLeaseJournal(paths, identity, realm, resources) {
|
|
76
|
+
return {
|
|
77
|
+
version: 1,
|
|
78
|
+
tenantHash: paths.tenantHash,
|
|
79
|
+
aicId: identity.id,
|
|
80
|
+
realm,
|
|
81
|
+
treeName: identity.treeName,
|
|
82
|
+
ownerToken: identity.ownerToken,
|
|
83
|
+
resources: resources.map((resource) => ({ ...resource })),
|
|
84
|
+
managedFixtures: [],
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
export async function readLeaseJournal(path) {
|
|
88
|
+
return readJson(path);
|
|
89
|
+
}
|
|
90
|
+
export async function writeLeaseJournal(path, journal) {
|
|
91
|
+
await mkdir(dirname(path), { recursive: true, mode: 0o700 });
|
|
92
|
+
const temporary = `${path}.${process.pid}.${randomUUID()}.tmp`;
|
|
93
|
+
await writeFile(temporary, JSON.stringify(journal), {
|
|
94
|
+
encoding: "utf8",
|
|
95
|
+
mode: 0o600,
|
|
96
|
+
flag: "wx",
|
|
97
|
+
});
|
|
98
|
+
await rename(temporary, path);
|
|
99
|
+
}
|
|
100
|
+
export async function removeLeaseJournal(path) {
|
|
101
|
+
await rm(path, { force: true });
|
|
102
|
+
}
|
|
103
|
+
export async function addJournalFixture(path, fixture) {
|
|
104
|
+
const journal = await requireJournal(path);
|
|
105
|
+
if (!journal.managedFixtures.some((item) => item.type === fixture.type && item.id === fixture.id)) {
|
|
106
|
+
journal.managedFixtures.push(fixture);
|
|
107
|
+
await writeLeaseJournal(path, journal);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
export async function addJournalResources(path, resources) {
|
|
111
|
+
const journal = await requireJournal(path);
|
|
112
|
+
for (const resource of resources) {
|
|
113
|
+
if (!journal.resources.some((item) => sameResource(item, resource))) {
|
|
114
|
+
journal.resources.push({ ...resource });
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
await writeLeaseJournal(path, journal);
|
|
118
|
+
}
|
|
119
|
+
export async function removeJournalFixture(path, fixture) {
|
|
120
|
+
const journal = await requireJournal(path);
|
|
121
|
+
journal.managedFixtures = journal.managedFixtures.filter((item) => item.type !== fixture.type || item.id !== fixture.id);
|
|
122
|
+
await writeLeaseJournal(path, journal);
|
|
123
|
+
}
|
|
124
|
+
async function requireJournal(path) {
|
|
125
|
+
const journal = await readLeaseJournal(path);
|
|
126
|
+
if (journal === undefined) {
|
|
127
|
+
throw new AicLaneError("AIC file lease ownership journal is missing");
|
|
128
|
+
}
|
|
129
|
+
return journal;
|
|
130
|
+
}
|
|
131
|
+
async function readJson(path) {
|
|
132
|
+
try {
|
|
133
|
+
return JSON.parse(await readFile(path, "utf8"));
|
|
134
|
+
}
|
|
135
|
+
catch (error) {
|
|
136
|
+
if (hasCode(error, "ENOENT")) {
|
|
137
|
+
return undefined;
|
|
138
|
+
}
|
|
139
|
+
throw new AicLaneError(`could not read AIC file lease state at ${path}`, {
|
|
140
|
+
cause: error,
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
function processExists(pid) {
|
|
145
|
+
try {
|
|
146
|
+
process.kill(pid, 0);
|
|
147
|
+
return true;
|
|
148
|
+
}
|
|
149
|
+
catch (error) {
|
|
150
|
+
return hasCode(error, "EPERM");
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
function sameResource(left, right) {
|
|
154
|
+
if (left.kind !== right.kind) {
|
|
155
|
+
return false;
|
|
156
|
+
}
|
|
157
|
+
if (left.kind === "tree" && right.kind === "tree") {
|
|
158
|
+
return left.name === right.name;
|
|
159
|
+
}
|
|
160
|
+
if (left.kind !== "tree" && right.kind !== "tree") {
|
|
161
|
+
return left.id === right.id;
|
|
162
|
+
}
|
|
163
|
+
return false;
|
|
164
|
+
}
|
|
165
|
+
function hasCode(error, code) {
|
|
166
|
+
return (typeof error === "object" &&
|
|
167
|
+
error !== null &&
|
|
168
|
+
"code" in error &&
|
|
169
|
+
error.code === code);
|
|
170
|
+
}
|
|
171
|
+
function delay(ms) {
|
|
172
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
173
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { Given, JsonObject } from "../case/types.ts";
|
|
2
|
+
import { type AicIo, type TenantSession } from "./tenant.ts";
|
|
3
|
+
export interface ManagedFixture {
|
|
4
|
+
type: string;
|
|
5
|
+
record: JsonObject;
|
|
6
|
+
}
|
|
7
|
+
export interface SeededManagedFixture {
|
|
8
|
+
type: string;
|
|
9
|
+
id: string;
|
|
10
|
+
}
|
|
11
|
+
/** Whether the first pass's managed seed came exactly from this fixture ledger. */
|
|
12
|
+
export declare function managedSeedMatches(managed: Given["managed"], fixtures: readonly ManagedFixture[]): boolean;
|
|
13
|
+
/** Create with CREST's create-only request, then prove every declared field landed. */
|
|
14
|
+
export declare function seedManagedFixtures(io: AicIo, session: TenantSession, fixtures: readonly ManagedFixture[], seeded: SeededManagedFixture[]): Promise<void>;
|
|
15
|
+
/** Delete one fixture the harness successfully created. A prior script delete is clean. */
|
|
16
|
+
export declare function deleteManagedFixture(io: AicIo, session: TenantSession, fixture: SeededManagedFixture): Promise<void>;
|
|
17
|
+
/**
|
|
18
|
+
* Serialize managed-fixture runs across worker processes for one tenant.
|
|
19
|
+
* A collision after this lock is acquired is therefore pre-existing state,
|
|
20
|
+
* not two files in this harness racing each other.
|
|
21
|
+
*/
|
|
22
|
+
export declare function acquireManagedFixtureLock(session: TenantSession, label: string): Promise<() => Promise<void>>;
|
|
23
|
+
export declare function fixtureIdentity(fixture: ManagedFixture): {
|
|
24
|
+
collection: string;
|
|
25
|
+
id: string;
|
|
26
|
+
resource: string;
|
|
27
|
+
};
|
|
28
|
+
export declare function managedResource(type: string, id: string): string;
|