@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.
Files changed (169) hide show
  1. package/LICENSE-APACHE +201 -0
  2. package/LICENSE-MIT +21 -0
  3. package/README.md +274 -0
  4. package/dist/bin/fetch-jar.d.ts +2 -0
  5. package/dist/bin/fetch-jar.js +13 -0
  6. package/dist/bin/pull-profile.d.ts +2 -0
  7. package/dist/bin/pull-profile.js +40 -0
  8. package/dist/bin/show-log.d.ts +2 -0
  9. package/dist/bin/show-log.js +31 -0
  10. package/dist/classes/.complete +12 -0
  11. package/dist/classes/AmClassShutter.class +0 -0
  12. package/dist/classes/AmContextFactory$ObservedContext.class +0 -0
  13. package/dist/classes/AmContextFactory.class +0 -0
  14. package/dist/classes/AmScopes.class +0 -0
  15. package/dist/classes/HostOps.class +0 -0
  16. package/dist/classes/JsValues.class +0 -0
  17. package/dist/classes/Json$Parser.class +0 -0
  18. package/dist/classes/Json.class +0 -0
  19. package/dist/classes/LanguageVersions.class +0 -0
  20. package/dist/classes/Runner.class +0 -0
  21. package/dist/classes/ScriptContextScope.class +0 -0
  22. package/dist/src/aic/callbacks.d.ts +35 -0
  23. package/dist/src/aic/callbacks.js +130 -0
  24. package/dist/src/aic/conform.d.ts +90 -0
  25. package/dist/src/aic/conform.js +193 -0
  26. package/dist/src/aic/constants.d.ts +13 -0
  27. package/dist/src/aic/constants.js +13 -0
  28. package/dist/src/aic/diff.d.ts +25 -0
  29. package/dist/src/aic/diff.js +201 -0
  30. package/dist/src/aic/emit-journey.d.ts +64 -0
  31. package/dist/src/aic/emit-journey.js +254 -0
  32. package/dist/src/aic/emit-result.d.ts +10 -0
  33. package/dist/src/aic/emit-result.js +39 -0
  34. package/dist/src/aic/emit-session.d.ts +26 -0
  35. package/dist/src/aic/emit-session.js +132 -0
  36. package/dist/src/aic/emit-subject.d.ts +40 -0
  37. package/dist/src/aic/emit-subject.js +210 -0
  38. package/dist/src/aic/file-lease.d.ts +35 -0
  39. package/dist/src/aic/file-lease.js +543 -0
  40. package/dist/src/aic/http.d.ts +21 -0
  41. package/dist/src/aic/http.js +127 -0
  42. package/dist/src/aic/idm.d.ts +10 -0
  43. package/dist/src/aic/idm.js +112 -0
  44. package/dist/src/aic/index.d.ts +35 -0
  45. package/dist/src/aic/index.js +21 -0
  46. package/dist/src/aic/js.d.ts +8 -0
  47. package/dist/src/aic/js.js +11 -0
  48. package/dist/src/aic/lease-identity.d.ts +38 -0
  49. package/dist/src/aic/lease-identity.js +72 -0
  50. package/dist/src/aic/lease-lock.d.ts +45 -0
  51. package/dist/src/aic/lease-lock.js +173 -0
  52. package/dist/src/aic/managed.d.ts +28 -0
  53. package/dist/src/aic/managed.js +199 -0
  54. package/dist/src/aic/provider.d.ts +156 -0
  55. package/dist/src/aic/provider.js +416 -0
  56. package/dist/src/aic/record.d.ts +28 -0
  57. package/dist/src/aic/record.js +183 -0
  58. package/dist/src/aic/resource-snapshot.d.ts +12 -0
  59. package/dist/src/aic/resource-snapshot.js +116 -0
  60. package/dist/src/aic/run.d.ts +102 -0
  61. package/dist/src/aic/run.js +450 -0
  62. package/dist/src/aic/tenant.d.ts +107 -0
  63. package/dist/src/aic/tenant.js +307 -0
  64. package/dist/src/aic/trace.d.ts +25 -0
  65. package/dist/src/aic/trace.js +56 -0
  66. package/dist/src/aic/txid.d.ts +11 -0
  67. package/dist/src/aic/txid.js +25 -0
  68. package/dist/src/aic/unsupported.d.ts +15 -0
  69. package/dist/src/aic/unsupported.js +60 -0
  70. package/dist/src/bindings/allowlist.d.ts +42 -0
  71. package/dist/src/bindings/allowlist.js +62 -0
  72. package/dist/src/bindings/harvest.d.ts +7 -0
  73. package/dist/src/bindings/harvest.js +137 -0
  74. package/dist/src/bindings/index.d.ts +4 -0
  75. package/dist/src/bindings/index.js +3 -0
  76. package/dist/src/bindings/preamble.d.ts +23 -0
  77. package/dist/src/bindings/preamble.js +36 -0
  78. package/dist/src/bindings/run.d.ts +33 -0
  79. package/dist/src/bindings/run.js +55 -0
  80. package/dist/src/case/equal.d.ts +2 -0
  81. package/dist/src/case/equal.js +40 -0
  82. package/dist/src/case/index.d.ts +5 -0
  83. package/dist/src/case/index.js +4 -0
  84. package/dist/src/case/portable.d.ts +10 -0
  85. package/dist/src/case/portable.js +28 -0
  86. package/dist/src/case/session.d.ts +34 -0
  87. package/dist/src/case/session.js +66 -0
  88. package/dist/src/case/state.d.ts +4 -0
  89. package/dist/src/case/state.js +47 -0
  90. package/dist/src/case/types.d.ts +266 -0
  91. package/dist/src/case/types.js +106 -0
  92. package/dist/src/case/util.d.ts +7 -0
  93. package/dist/src/case/util.js +115 -0
  94. package/dist/src/case/validate.d.ts +8 -0
  95. package/dist/src/case/validate.js +463 -0
  96. package/dist/src/case/verdict.d.ts +6 -0
  97. package/dist/src/case/verdict.js +810 -0
  98. package/dist/src/diagnostics.d.ts +7 -0
  99. package/dist/src/diagnostics.js +5 -0
  100. package/dist/src/emit-dts.d.ts +2 -0
  101. package/dist/src/emit-dts.js +82 -0
  102. package/dist/src/emit-js.d.ts +3 -0
  103. package/dist/src/emit-js.js +149 -0
  104. package/dist/src/harness/failures.d.ts +26 -0
  105. package/dist/src/harness/failures.js +113 -0
  106. package/dist/src/harness/idm.d.ts +12 -0
  107. package/dist/src/harness/idm.js +44 -0
  108. package/dist/src/harness/index.d.ts +11 -0
  109. package/dist/src/harness/index.js +6 -0
  110. package/dist/src/harness/lease.d.ts +136 -0
  111. package/dist/src/harness/lease.js +286 -0
  112. package/dist/src/harness/residue.d.ts +23 -0
  113. package/dist/src/harness/residue.js +73 -0
  114. package/dist/src/harness/show-log.d.ts +43 -0
  115. package/dist/src/harness/show-log.js +253 -0
  116. package/dist/src/harness/spec.d.ts +67 -0
  117. package/dist/src/harness/spec.js +189 -0
  118. package/dist/src/harness/step.d.ts +80 -0
  119. package/dist/src/harness/step.js +97 -0
  120. package/dist/src/harness/types.d.ts +100 -0
  121. package/dist/src/harness/types.js +1 -0
  122. package/dist/src/harness/vitest.d.ts +52 -0
  123. package/dist/src/harness/vitest.js +155 -0
  124. package/dist/src/idents.d.ts +4 -0
  125. package/dist/src/idents.js +42 -0
  126. package/dist/src/jvm.d.ts +124 -0
  127. package/dist/src/jvm.js +432 -0
  128. package/dist/src/load.d.ts +2 -0
  129. package/dist/src/load.js +104 -0
  130. package/dist/src/paths.d.ts +36 -0
  131. package/dist/src/paths.js +50 -0
  132. package/dist/src/profile/index.d.ts +5 -0
  133. package/dist/src/profile/index.js +3 -0
  134. package/dist/src/profile/normalise.d.ts +16 -0
  135. package/dist/src/profile/normalise.js +106 -0
  136. package/dist/src/profile/pull.d.ts +22 -0
  137. package/dist/src/profile/pull.js +36 -0
  138. package/dist/src/profile/seed.d.ts +11 -0
  139. package/dist/src/profile/seed.js +28 -0
  140. package/dist/src/profile/store.d.ts +13 -0
  141. package/dist/src/profile/store.js +60 -0
  142. package/dist/src/profile/types.d.ts +45 -0
  143. package/dist/src/profile/types.js +1 -0
  144. package/dist/src/profile/validate.d.ts +38 -0
  145. package/dist/src/profile/validate.js +129 -0
  146. package/dist/src/project.d.ts +37 -0
  147. package/dist/src/project.js +93 -0
  148. package/dist/src/protocol.d.ts +62 -0
  149. package/dist/src/protocol.js +68 -0
  150. package/dist/src/provider-module.d.ts +11 -0
  151. package/dist/src/provider-module.js +46 -0
  152. package/dist/src/runner.d.ts +39 -0
  153. package/dist/src/runner.js +337 -0
  154. package/dist/src/schema.d.ts +48 -0
  155. package/dist/src/schema.js +93 -0
  156. package/generated/scripted-decision-mocks.cjs +722 -0
  157. package/generated/scripted-decision-mocks.d.ts +626 -0
  158. package/generated/scripted-decision-next.json +3640 -0
  159. package/java/AmClassShutter.java +98 -0
  160. package/java/AmContextFactory.java +69 -0
  161. package/java/AmScopes.java +49 -0
  162. package/java/HostOps.java +180 -0
  163. package/java/JsValues.java +39 -0
  164. package/java/Json.java +441 -0
  165. package/java/LanguageVersions.java +55 -0
  166. package/java/Runner.java +289 -0
  167. package/java/ScriptContextScope.java +147 -0
  168. package/package.json +88 -0
  169. package/src/bindings/rhino/runtime.cjs +3021 -0
@@ -0,0 +1,107 @@
1
+ import { type HttpRequest, type HttpResponse } from "./http.ts";
2
+ import { type TenantProvider } from "./provider.ts";
3
+ export interface TenantSession {
4
+ tenantName: string;
5
+ baseUrl: string;
6
+ token: string;
7
+ /** Where `token` came from, and where a replacement comes from after a 401. */
8
+ provider: TenantProvider;
9
+ }
10
+ export interface CliResult {
11
+ status: number;
12
+ stdout: string;
13
+ stderr: string;
14
+ }
15
+ export interface AmResponse {
16
+ status: number;
17
+ bodyText: string;
18
+ body: unknown;
19
+ transactionId?: string;
20
+ }
21
+ export interface AmRequest {
22
+ method: string;
23
+ path: string;
24
+ headers?: Array<[string, string]>;
25
+ body?: string;
26
+ /** When true, do not send the service-account bearer (authenticate). */
27
+ anonymous?: boolean;
28
+ timeoutMs?: number;
29
+ }
30
+ export interface AicIo {
31
+ aic(args: string[]): Promise<CliResult>;
32
+ http(req: HttpRequest): Promise<HttpResponse>;
33
+ }
34
+ export declare const AM_CONFIG_API_VERSION = "protocol=2.0,resource=1.0";
35
+ export declare function amConfigHeaders(): Array<[string, string]>;
36
+ export declare class AicLaneError extends Error {
37
+ readonly status?: number;
38
+ readonly transactionId?: string;
39
+ constructor(message: string, options?: {
40
+ status?: number;
41
+ transactionId?: string;
42
+ cause?: unknown;
43
+ });
44
+ }
45
+ export declare function defaultAicIo(project: string): AicIo;
46
+ export interface ConnectOptions {
47
+ /**
48
+ * An `aic` context name. With the `aic` provider it selects the context;
49
+ * with any other it must equal the provider's name, so a suite pinned to
50
+ * one tenant cannot run against another.
51
+ */
52
+ tenant?: string;
53
+ /** The `aic` project directory; the `aic` provider only. */
54
+ project?: string;
55
+ /** Default: {@link configuredTenantProvider}, else the `aic` CLI. */
56
+ provider?: TenantProvider;
57
+ /**
58
+ * `false` skips the registered and environment providers. Callers pass it
59
+ * when they were handed an `io`: an injected `io` is a test seam for both
60
+ * `aic` and HTTP, and a configured service account would mint its token
61
+ * over the real network behind it.
62
+ */
63
+ useConfigured?: boolean;
64
+ }
65
+ /**
66
+ * The provider a connection uses: the one passed, else the one registered
67
+ * with `setTenantProvider` or configured by `AIC_SCRIPT_TESTER_TENANT_URL`, else
68
+ * this repo's `aic` agent.
69
+ */
70
+ export declare function resolveTenantProvider(io: AicIo, options?: ConnectOptions): TenantProvider;
71
+ export declare function connectTenant(io: AicIo, options?: ConnectOptions): Promise<TenantSession>;
72
+ /**
73
+ * This repo's provider: the tenant and bearer come from the `aic` agent
74
+ * (`aic ctx list --json`, `aic whoami --token`), and logs from
75
+ * `aic logs tx --wait`. The agent must be unlocked; every call passes
76
+ * `--no-prompt` so a locked one fails instead of waiting for a password.
77
+ */
78
+ export declare function aicCliProvider(io: AicIo, options?: {
79
+ tenant?: string;
80
+ project?: string;
81
+ }): TenantProvider;
82
+ /**
83
+ * Ask the provider for a replacement bearer and adopt it.
84
+ *
85
+ * The history below is the `aic` provider's, and it is why a 401 must reach
86
+ * the provider as `"rejected"`: a provider that caches has to know the token
87
+ * it holds is dead, rather than handing it back.
88
+ *
89
+ * MEASURED 2026-09-14 over 20 minutes: `aic whoami --token` handed back a
90
+ * token that worked *right now* and said nothing about how much life was left
91
+ * in it. The first token this probe was given was rejected 401 five minutes
92
+ * later, while a token fetched moments after it stayed good for the next ten —
93
+ * the agent rotated on its own ~898s schedule, so what you got depended on
94
+ * where in that cycle you asked.
95
+ *
96
+ * `aic` now guarantees `--token` at least 840 of those seconds, minting when
97
+ * the cached token is below the floor (MEASURED 2026-09-15: with 823s left the
98
+ * TTL jumped to 899 across the call, and with 864s left it did not move). That
99
+ * removes the reason this refresh usually fired, and none of the reasons it
100
+ * must stay: a file may run longer than the floor, the tenant may revoke, and
101
+ * an older `aic` on someone else's PATH makes no such promise. So the 401 retry
102
+ * remains the correctness mechanism and the floor is only what makes it rare.
103
+ */
104
+ export declare function refreshSessionToken(session: TenantSession): Promise<void>;
105
+ export declare function realmJsonPath(realm: string): string;
106
+ export declare function amRequest(io: AicIo, session: TenantSession, req: AmRequest): Promise<AmResponse>;
107
+ export declare function redact(text: string, session: TenantSession): string;
@@ -0,0 +1,307 @@
1
+ import { execFile } from "node:child_process";
2
+ import { promisify } from "node:util";
3
+ import { join } from "node:path";
4
+ import { isPlainObject } from "../case/util.js";
5
+ import { projectRoot } from "../project.js";
6
+ import { headerValues, sendHttp } from "./http.js";
7
+ import { checkTenantDescription, configuredTenantProvider, TENANT_ENV, } from "./provider.js";
8
+ const execFileAsync = promisify(execFile);
9
+ export const AM_CONFIG_API_VERSION = "protocol=2.0,resource=1.0";
10
+ export function amConfigHeaders() {
11
+ return [["Accept-API-Version", AM_CONFIG_API_VERSION]];
12
+ }
13
+ export class AicLaneError extends Error {
14
+ status;
15
+ transactionId;
16
+ constructor(message, options = {}) {
17
+ super(message, options.cause !== undefined ? { cause: options.cause } : {});
18
+ this.name = "AicLaneError";
19
+ if (options.status !== undefined) {
20
+ this.status = options.status;
21
+ }
22
+ if (options.transactionId !== undefined) {
23
+ this.transactionId = options.transactionId;
24
+ }
25
+ }
26
+ }
27
+ export function defaultAicIo(project) {
28
+ const bin = process.env.AIC_BIN ?? join(project, "target", "debug", "aic");
29
+ return {
30
+ async aic(args) {
31
+ try {
32
+ const result = await execFileAsync(bin, args, {
33
+ cwd: project,
34
+ timeout: 30_000,
35
+ encoding: "utf8",
36
+ maxBuffer: 2_000_000,
37
+ });
38
+ return { status: 0, stdout: result.stdout, stderr: result.stderr };
39
+ }
40
+ catch (error) {
41
+ if (isExecError(error) && error.code === "ENOENT") {
42
+ throw new AicLaneError(`no tenant provider configured: pass \`provider\`, call setTenantProvider(), ` +
43
+ `or set ${TENANT_ENV.url} (see docs/rhino-local-harness.md); ` +
44
+ `the aic fallback needs the aic CLI, and ${bin} does not exist (set AIC_BIN)`, { cause: error });
45
+ }
46
+ if (isExecError(error)) {
47
+ return {
48
+ status: error.code === null || error.code === undefined ? 1 : execStatus(error.code),
49
+ stdout: error.stdout ?? "",
50
+ stderr: error.stderr ?? error.message,
51
+ };
52
+ }
53
+ throw error;
54
+ }
55
+ },
56
+ http: sendHttp,
57
+ };
58
+ }
59
+ function isExecError(error) {
60
+ return error instanceof Error;
61
+ }
62
+ function execStatus(code) {
63
+ return typeof code === "number" ? code : 1;
64
+ }
65
+ /**
66
+ * The provider a connection uses: the one passed, else the one registered
67
+ * with `setTenantProvider` or configured by `AIC_SCRIPT_TESTER_TENANT_URL`, else
68
+ * this repo's `aic` agent.
69
+ */
70
+ export function resolveTenantProvider(io, options = {}) {
71
+ return (options.provider ??
72
+ (options.useConfigured === false ? undefined : configuredTenantProvider()) ??
73
+ aicCliProvider(io, {
74
+ ...(options.tenant === undefined ? {} : { tenant: options.tenant }),
75
+ ...(options.project === undefined ? {} : { project: options.project }),
76
+ }));
77
+ }
78
+ export async function connectTenant(io, options = {}) {
79
+ const provider = resolveTenantProvider(io, options);
80
+ const description = checkTenantDescription(await provider.describe());
81
+ if (options.tenant !== undefined && options.tenant !== description.name) {
82
+ throw new AicLaneError(
83
+ // Neither name is quoted: a provider's name defaults to its hostname.
84
+ "the tenant provider serves a different tenant from the one this run asks for (the `tenant` option)");
85
+ }
86
+ const token = await provider.getToken({ reason: "initial" });
87
+ return {
88
+ tenantName: description.name,
89
+ baseUrl: stripSlash(description.baseUrl),
90
+ token,
91
+ provider,
92
+ };
93
+ }
94
+ /**
95
+ * This repo's provider: the tenant and bearer come from the `aic` agent
96
+ * (`aic ctx list --json`, `aic whoami --token`), and logs from
97
+ * `aic logs tx --wait`. The agent must be unlocked; every call passes
98
+ * `--no-prompt` so a locked one fails instead of waiting for a password.
99
+ */
100
+ export function aicCliProvider(io, options = {}) {
101
+ const project = options.project ?? projectRoot();
102
+ const noPrompt = ["--no-prompt", "--project", project];
103
+ let chosen;
104
+ function describe() {
105
+ chosen ??= chooseContext(io, noPrompt, options.tenant);
106
+ return chosen;
107
+ }
108
+ const logs = {
109
+ async transaction(id, readOptions = {}) {
110
+ const { name } = await describe();
111
+ const args = [...noPrompt, "logs", "tx", id, "--tenant", name, "--wait"];
112
+ if (readOptions.waitMs !== undefined) {
113
+ args.push("--timeout", String(Math.max(1, Math.ceil(readOptions.waitMs / 1000))));
114
+ }
115
+ const result = await io.aic(args);
116
+ if (result.status !== 0) {
117
+ throw new AicLaneError(formatLogsFailure(result), { status: result.status });
118
+ }
119
+ let parsed;
120
+ try {
121
+ parsed = JSON.parse(result.stdout);
122
+ }
123
+ catch (error) {
124
+ throw new AicLaneError("aic logs tx did not print JSON", { cause: error });
125
+ }
126
+ if (!Array.isArray(parsed)) {
127
+ throw new AicLaneError("aic logs tx did not print a JSON array");
128
+ }
129
+ return parsed;
130
+ },
131
+ };
132
+ return {
133
+ describe,
134
+ async getToken(request) {
135
+ const { name } = await describe();
136
+ const whoami = await io.aic([...noPrompt, "whoami", "--token", "--tenant", name]);
137
+ const during = request.reason === "rejected" ? " while refreshing the bearer" : "";
138
+ if (whoami.status !== 0) {
139
+ throw new AicLaneError(`aic whoami --token failed${during} (status ${whoami.status}): ${trim(whoami.stderr || whoami.stdout)}. Unlock the agent (aic login) or pass a reachable tenant.`);
140
+ }
141
+ const token = whoami.stdout.trim();
142
+ if (token.length === 0) {
143
+ throw new AicLaneError(`aic whoami --token printed an empty token${during}`);
144
+ }
145
+ return token;
146
+ },
147
+ logs,
148
+ };
149
+ }
150
+ async function chooseContext(io, noPrompt, tenant) {
151
+ const list = await io.aic([...noPrompt, "ctx", "list", "--json"]);
152
+ if (list.status !== 0) {
153
+ throw new AicLaneError(`aic ctx list failed (status ${list.status}): ${trim(list.stderr || list.stdout)}`);
154
+ }
155
+ const tenants = parseCtxList(list.stdout);
156
+ const chosen = tenant !== undefined
157
+ ? tenants.find((row) => row.name === tenant)
158
+ : tenants.find((row) => row.current) ?? tenants[0];
159
+ if (chosen === undefined) {
160
+ throw new AicLaneError(tenant === undefined
161
+ ? "aic ctx list returned no tenants"
162
+ : `aic ctx list has no tenant named ${JSON.stringify(tenant)}`);
163
+ }
164
+ return { name: chosen.name, baseUrl: stripSlash(chosen.base_url) };
165
+ }
166
+ function formatLogsFailure(result) {
167
+ const body = (result.stderr || result.stdout).trim();
168
+ const hint = "Unlock the agent (`aic login`) if it is locked.";
169
+ if (body.length === 0) {
170
+ return `aic logs tx failed (status ${result.status}). ${hint}`;
171
+ }
172
+ if (/locked|aic login|session login/i.test(body)) {
173
+ return body;
174
+ }
175
+ return `${body}\n${hint}`;
176
+ }
177
+ function parseCtxList(stdout) {
178
+ let parsed;
179
+ try {
180
+ parsed = JSON.parse(stdout);
181
+ }
182
+ catch (error) {
183
+ const message = error instanceof Error ? error.message : String(error);
184
+ throw new AicLaneError(`aic ctx list --json is not JSON: ${message}`);
185
+ }
186
+ if (!Array.isArray(parsed)) {
187
+ throw new AicLaneError("aic ctx list --json is not an array");
188
+ }
189
+ const rows = [];
190
+ for (const item of parsed) {
191
+ if (!isPlainObject(item) || typeof item.name !== "string" || typeof item.base_url !== "string") {
192
+ throw new AicLaneError("aic ctx list --json row is missing name/base_url");
193
+ }
194
+ rows.push({
195
+ current: item.current === true,
196
+ name: item.name,
197
+ base_url: item.base_url,
198
+ });
199
+ }
200
+ return rows;
201
+ }
202
+ /**
203
+ * Ask the provider for a replacement bearer and adopt it.
204
+ *
205
+ * The history below is the `aic` provider's, and it is why a 401 must reach
206
+ * the provider as `"rejected"`: a provider that caches has to know the token
207
+ * it holds is dead, rather than handing it back.
208
+ *
209
+ * MEASURED 2026-09-14 over 20 minutes: `aic whoami --token` handed back a
210
+ * token that worked *right now* and said nothing about how much life was left
211
+ * in it. The first token this probe was given was rejected 401 five minutes
212
+ * later, while a token fetched moments after it stayed good for the next ten —
213
+ * the agent rotated on its own ~898s schedule, so what you got depended on
214
+ * where in that cycle you asked.
215
+ *
216
+ * `aic` now guarantees `--token` at least 840 of those seconds, minting when
217
+ * the cached token is below the floor (MEASURED 2026-09-15: with 823s left the
218
+ * TTL jumped to 899 across the call, and with 864s left it did not move). That
219
+ * removes the reason this refresh usually fired, and none of the reasons it
220
+ * must stay: a file may run longer than the floor, the tenant may revoke, and
221
+ * an older `aic` on someone else's PATH makes no such promise. So the 401 retry
222
+ * remains the correctness mechanism and the floor is only what makes it rare.
223
+ */
224
+ export async function refreshSessionToken(session) {
225
+ session.token = await session.provider.getToken({
226
+ reason: "rejected",
227
+ rejected: session.token,
228
+ });
229
+ }
230
+ export function realmJsonPath(realm) {
231
+ return `/am/json/realms/root/realms/${realm}`;
232
+ }
233
+ export async function amRequest(io, session, req) {
234
+ const first = await sendAmRequest(io, session, req);
235
+ // A 401 on an authenticated config call means the bearer died under us, not
236
+ // that the request was wrong — so refresh once and retry. Anonymous calls
237
+ // are excluded deliberately: `/authenticate` answers a journey that reached
238
+ // no declared outcome with a bare 401, and that is a verdict, not an
239
+ // expired token.
240
+ if (first.status !== 401 || req.anonymous === true) {
241
+ return first;
242
+ }
243
+ await refreshSessionToken(session);
244
+ return sendAmRequest(io, session, req);
245
+ }
246
+ async function sendAmRequest(io, session, req) {
247
+ const url = `${session.baseUrl}${req.path}`;
248
+ const headers = [
249
+ ["Accept", "application/json"],
250
+ ["Content-Type", "application/json"],
251
+ ...(req.headers ?? []),
252
+ ];
253
+ if (!req.anonymous) {
254
+ headers.push(["Authorization", `Bearer ${session.token}`]);
255
+ headers.push(["X-Requested-With", "XMLHttpRequest"]);
256
+ }
257
+ const response = await io.http({
258
+ url,
259
+ method: req.method,
260
+ headerLines: headers,
261
+ ...(req.body !== undefined ? { body: req.body } : {}),
262
+ ...(req.timeoutMs !== undefined ? { timeoutMs: req.timeoutMs } : {}),
263
+ });
264
+ const transactionId = headerValues(response.headers, "x-forgerock-transactionid")[0];
265
+ const bodyText = redact(response.body, session);
266
+ let body = null;
267
+ if (response.body.length > 0) {
268
+ try {
269
+ body = JSON.parse(response.body);
270
+ }
271
+ catch {
272
+ body = bodyText;
273
+ }
274
+ }
275
+ return {
276
+ status: response.status,
277
+ bodyText,
278
+ body,
279
+ ...(transactionId !== undefined ? { transactionId } : {}),
280
+ };
281
+ }
282
+ export function redact(text, session) {
283
+ let out = text;
284
+ const secrets = [session.token, session.baseUrl, hostnameOf(session.baseUrl)];
285
+ for (const secret of secrets) {
286
+ if (secret.length === 0) {
287
+ continue;
288
+ }
289
+ out = out.split(secret).join("<redacted>");
290
+ }
291
+ return out;
292
+ }
293
+ function hostnameOf(baseUrl) {
294
+ try {
295
+ return new URL(baseUrl).hostname;
296
+ }
297
+ catch {
298
+ return "";
299
+ }
300
+ }
301
+ function stripSlash(url) {
302
+ return url.endsWith("/") ? url.slice(0, -1) : url;
303
+ }
304
+ function trim(text) {
305
+ const sliced = text.trim().replace(/\s+/g, " ");
306
+ return sliced.length > 400 ? `${sliced.slice(0, 400)}…` : sliced;
307
+ }
@@ -0,0 +1,25 @@
1
+ /**
2
+ * One AIC-lane run's transaction ids, parked for the vitest hook to pick up
3
+ * if the test fails. Local-only tests never write one of these.
4
+ */
5
+ export interface AicTrace {
6
+ stem: string;
7
+ passIds: string[];
8
+ tenantName: string;
9
+ }
10
+ export declare function noteAicTrace(trace: AicTrace): void;
11
+ export declare function appendAicPass(id: string): void;
12
+ export declare function peekAicTrace(): AicTrace | undefined;
13
+ export declare function takeAicTrace(): AicTrace | undefined;
14
+ export declare function clearAicTrace(): void;
15
+ /** Subject chain: numbered passes under one stem. */
16
+ export declare function beginTrace(tenantName: string, stem?: string): {
17
+ stem: string;
18
+ next: () => string;
19
+ };
20
+ /**
21
+ * A one-off authenticate that is not a numbered subject pass (the session
22
+ * minter). Published so a mint failure is still look-up-able; the subject
23
+ * trace replaces it if minting succeeds.
24
+ */
25
+ export declare function beginSingleTrace(tenantName: string, id?: string): string;
@@ -0,0 +1,56 @@
1
+ import { newStem, passId } from "./txid.js";
2
+ let current;
3
+ export function noteAicTrace(trace) {
4
+ current = {
5
+ stem: trace.stem,
6
+ passIds: [...trace.passIds],
7
+ tenantName: trace.tenantName,
8
+ };
9
+ }
10
+ export function appendAicPass(id) {
11
+ if (current === undefined) {
12
+ throw new Error("rhino-local: appendAicPass without an active AIC trace");
13
+ }
14
+ current.passIds.push(id);
15
+ }
16
+ export function peekAicTrace() {
17
+ if (current === undefined) {
18
+ return undefined;
19
+ }
20
+ return {
21
+ stem: current.stem,
22
+ passIds: [...current.passIds],
23
+ tenantName: current.tenantName,
24
+ };
25
+ }
26
+ export function takeAicTrace() {
27
+ const out = peekAicTrace();
28
+ current = undefined;
29
+ return out;
30
+ }
31
+ export function clearAicTrace() {
32
+ current = undefined;
33
+ }
34
+ /** Subject chain: numbered passes under one stem. */
35
+ export function beginTrace(tenantName, stem = newStem()) {
36
+ noteAicTrace({ stem, passIds: [], tenantName });
37
+ let step = 0;
38
+ return {
39
+ stem,
40
+ next() {
41
+ step += 1;
42
+ const id = passId(stem, step);
43
+ appendAicPass(id);
44
+ return id;
45
+ },
46
+ };
47
+ }
48
+ /**
49
+ * A one-off authenticate that is not a numbered subject pass (the session
50
+ * minter). Published so a mint failure is still look-up-able; the subject
51
+ * trace replaces it if minting succeeds.
52
+ */
53
+ export function beginSingleTrace(tenantName, id = newStem()) {
54
+ noteAicTrace({ stem: id, passIds: [id], tenantName });
55
+ return id;
56
+ }
@@ -0,0 +1,11 @@
1
+ /**
2
+ * AM's `/monitoring/logs?transactionId=` query is a prefix match
3
+ * (`docs/api/08-logs.md`). Unpadded `-1` silently absorbs `-10`. Two-digit
4
+ * padding keeps each pass separately addressable; the cap is the cost.
5
+ */
6
+ export declare const MAX_PASSES = 99;
7
+ export declare const PASS_DIGITS = 2;
8
+ /** Request header AM accepts; the supplied value wins (`docs/api/02-headers-and-versioning.md`). */
9
+ export declare const TX_HEADER = "x-forgerock-transactionid";
10
+ export declare function newStem(): string;
11
+ export declare function passId(stem: string, step: number): string;
@@ -0,0 +1,25 @@
1
+ import { randomUUID } from "node:crypto";
2
+ /**
3
+ * AM's `/monitoring/logs?transactionId=` query is a prefix match
4
+ * (`docs/api/08-logs.md`). Unpadded `-1` silently absorbs `-10`. Two-digit
5
+ * padding keeps each pass separately addressable; the cap is the cost.
6
+ */
7
+ export const MAX_PASSES = 99;
8
+ export const PASS_DIGITS = 2;
9
+ /** Request header AM accepts; the supplied value wins (`docs/api/02-headers-and-versioning.md`). */
10
+ export const TX_HEADER = "x-forgerock-transactionid";
11
+ export function newStem() {
12
+ return randomUUID();
13
+ }
14
+ export function passId(stem, step) {
15
+ if (stem.length === 0) {
16
+ throw new Error("rhino-local: transaction stem must be non-empty");
17
+ }
18
+ if (!Number.isInteger(step) || step < 1) {
19
+ throw new Error(`rhino-local: transaction step must be an integer >= 1, got ${String(step)}`);
20
+ }
21
+ if (step > MAX_PASSES) {
22
+ throw new Error(`rhino-local: transaction step ${step} exceeds ${MAX_PASSES} — two-digit padding would wrap into an id that is a prefix of another`);
23
+ }
24
+ return `${stem}-${String(step).padStart(PASS_DIGITS, "0")}`;
25
+ }
@@ -0,0 +1,15 @@
1
+ import type { Case } from "../case/types.ts";
2
+ /**
3
+ * Why this case must not be executed on a tenant. `undefined` means the
4
+ * wrapper journey can seed `given` faithfully enough that a green AIC run
5
+ * is evidence, not coincidence.
6
+ *
7
+ * Environment-dependent inputs (`esv` / `secrets` / `managed` / `http`) are
8
+ * skipped rather than silently running against whatever the tenant holds.
9
+ * The one exception is managed state whose fixture-ledger provenance was
10
+ * checked by the caller; later chain passes may then carry records the subject
11
+ * itself created without turning the tenant lane back off.
12
+ */
13
+ export declare function aicUnsupportedReason(kase: Case, options?: {
14
+ harnessOwnsManaged?: boolean;
15
+ }): string | undefined;
@@ -0,0 +1,60 @@
1
+ import { isPortable } from "../case/portable.js";
2
+ import { ENV_INPUT_KEYS } from "../case/types.js";
3
+ function objectHasKeys(value) {
4
+ return value !== undefined && Object.keys(value).length > 0;
5
+ }
6
+ /**
7
+ * Why this case must not be executed on a tenant. `undefined` means the
8
+ * wrapper journey can seed `given` faithfully enough that a green AIC run
9
+ * is evidence, not coincidence.
10
+ *
11
+ * Environment-dependent inputs (`esv` / `secrets` / `managed` / `http`) are
12
+ * skipped rather than silently running against whatever the tenant holds.
13
+ * The one exception is managed state whose fixture-ledger provenance was
14
+ * checked by the caller; later chain passes may then carry records the subject
15
+ * itself created without turning the tenant lane back off.
16
+ */
17
+ export function aicUnsupportedReason(kase, options = {}) {
18
+ if (kase.given.engine === "legacy") {
19
+ return "legacy engine: AIC wrapper emit is next-gen only (legacy results go through JavaImporter + Action.send, not callbacksBuilder)";
20
+ }
21
+ if (!isPortable(kase)) {
22
+ const declared = ENV_INPUT_KEYS.filter((key) => hasDeclaredEnv(kase.given, key) &&
23
+ !(key === "managed" && options.harnessOwnsManaged === true));
24
+ if (declared.length > 0) {
25
+ return `given.${declared.join(", ")} is environment-dependent; AIC lane skips rather than run against whatever the tenant holds`;
26
+ }
27
+ }
28
+ if (kase.given.resumedFromSuspend === true) {
29
+ return "given.resumedFromSuspend=true cannot be seeded on a first authenticate";
30
+ }
31
+ if (objectHasKeys(kase.given.secureState)) {
32
+ return "given.secureState: next-gen nodeState has no putSecure";
33
+ }
34
+ if (kase.given.scriptName !== undefined) {
35
+ return "given.scriptName cannot be seeded on AIC (it is the uploaded script's name)";
36
+ }
37
+ if (kase.given.cookieName !== undefined) {
38
+ return "given.cookieName cannot be seeded on AIC (it is a tenant serverinfo value)";
39
+ }
40
+ if (objectHasKeys(kase.given.locales)) {
41
+ return "given.locales cannot be seeded by a setup script";
42
+ }
43
+ if (objectHasKeys(kase.given.bindings)) {
44
+ return "given.bindings cannot be seeded on AIC (those are engine bindings, not nodeState)";
45
+ }
46
+ return undefined;
47
+ }
48
+ function hasDeclaredEnv(given, key) {
49
+ const value = given[key];
50
+ if (value === undefined || value === null) {
51
+ return false;
52
+ }
53
+ if (Array.isArray(value)) {
54
+ return value.length > 0;
55
+ }
56
+ if (typeof value === "object") {
57
+ return Object.keys(value).length > 0;
58
+ }
59
+ return true;
60
+ }
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Boxed value types AM permits that its declared `allowLists` does not name.
3
+ *
4
+ * NOT from the descriptor, and kept separate from it so the audit trail shows
5
+ * which half of the policy is documented and which is not.
6
+ *
7
+ * The descriptor names `java.lang.Byte`, `Character`, `Float`, `Long`,
8
+ * `Number` and `Short` — but not `Double`, `Integer`, `Boolean` or `String`.
9
+ * That reads as an omission in AM's descriptor rather than a policy, because
10
+ * enforcing it would break rows the live matrix records as WORKING:
11
+ *
12
+ * - `java.util.Collections.*` is a live ✅ on the decision node, yet
13
+ * `Collections.singletonMap("a", 1).get("a")` needs `java.lang.Double` —
14
+ * Rhino converts a JS number to a Double. Without it the call fails with
15
+ * `Access to Java class "java.lang.Double" is prohibited`.
16
+ * - An index loop is what `docs/api/12-script-bindings-matrix.md` calls "the
17
+ * portable answer" for a Java List, and `list.get(0)` on a list of strings
18
+ * hands back a `java.lang.String`.
19
+ *
20
+ * The same page records the matching anomaly in the other context:
21
+ * `java.lang.String` and `java.lang.Integer` resolve in validate-scope although
22
+ * neither is declared there — "the rule behind that split is not established".
23
+ *
24
+ * Scoped to the types Rhino produces converting JS primitives across the
25
+ * boundary, rather than a speculative widening.
26
+ *
27
+ * STATUS: inferred from live-recorded rows, NOT directly measured on a decision
28
+ * node. `packages/rhino-local` has a probe ready; measure and then either
29
+ * fold these into the descriptor's story or drop them.
30
+ */
31
+ export declare const OBSERVED_BASE: readonly string[];
32
+ /**
33
+ * The scripted decision node's Java class allow-list: the descriptor's declared
34
+ * entries plus `OBSERVED_BASE`.
35
+ *
36
+ * Read from the binding descriptor this project already extracted from AM
37
+ * (`docs/api/bindings/scripted-decision-next.json`), so the policy has one
38
+ * source and cannot drift from the document the rest of the harness generates
39
+ * its mocks from. Not hand-transcribed: a copied list is a list that goes
40
+ * stale silently.
41
+ */
42
+ export declare function decisionNodeClassAllowList(): string[];