@codyswann/lisa 2.259.4 → 2.260.1

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 (79) hide show
  1. package/all/copy-contents/gitignore +4 -0
  2. package/dist/cli/file-upstream-cmd.d.ts +28 -0
  3. package/dist/cli/file-upstream-cmd.d.ts.map +1 -0
  4. package/dist/cli/file-upstream-cmd.js +231 -0
  5. package/dist/cli/file-upstream-cmd.js.map +1 -0
  6. package/dist/cli/gate-commands.d.ts +17 -0
  7. package/dist/cli/gate-commands.d.ts.map +1 -0
  8. package/dist/cli/gate-commands.js +47 -0
  9. package/dist/cli/gate-commands.js.map +1 -0
  10. package/dist/cli/index.d.ts +3 -0
  11. package/dist/cli/index.d.ts.map +1 -1
  12. package/dist/cli/index.js +6 -27
  13. package/dist/cli/index.js.map +1 -1
  14. package/dist/core/upstream-attribution-body.d.ts +38 -25
  15. package/dist/core/upstream-attribution-body.d.ts.map +1 -1
  16. package/dist/core/upstream-attribution-body.js +515 -155
  17. package/dist/core/upstream-attribution-body.js.map +1 -1
  18. package/dist/core/upstream-evidence-manifest.d.ts +7 -0
  19. package/dist/core/upstream-evidence-manifest.d.ts.map +1 -0
  20. package/dist/core/upstream-evidence-manifest.js +11623 -0
  21. package/dist/core/upstream-evidence-manifest.js.map +1 -0
  22. package/package.json +4 -1
  23. package/plugins/lisa/.claude-plugin/plugin.json +1 -1
  24. package/plugins/lisa/.codex-plugin/plugin.json +1 -1
  25. package/plugins/lisa/.codex-plugin/skills/lisa-persist-learning/SKILL.md +32 -8
  26. package/plugins/lisa/skills/lisa-persist-learning/SKILL.md +32 -8
  27. package/plugins/lisa-agy/plugin.json +1 -1
  28. package/plugins/lisa-agy/skills/lisa-persist-learning/SKILL.md +32 -8
  29. package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
  30. package/plugins/lisa-cdk/.codex-plugin/plugin.json +1 -1
  31. package/plugins/lisa-cdk-agy/plugin.json +1 -1
  32. package/plugins/lisa-cdk-copilot/.claude-plugin/plugin.json +1 -1
  33. package/plugins/lisa-cdk-cursor/.claude-plugin/plugin.json +1 -1
  34. package/plugins/lisa-copilot/.claude-plugin/plugin.json +1 -1
  35. package/plugins/lisa-copilot/skills/lisa-persist-learning/SKILL.md +32 -8
  36. package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
  37. package/plugins/lisa-cursor/skills/lisa-persist-learning/SKILL.md +32 -8
  38. package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
  39. package/plugins/lisa-expo/.codex-plugin/plugin.json +1 -1
  40. package/plugins/lisa-expo-agy/plugin.json +1 -1
  41. package/plugins/lisa-expo-copilot/.claude-plugin/plugin.json +1 -1
  42. package/plugins/lisa-expo-cursor/.claude-plugin/plugin.json +1 -1
  43. package/plugins/lisa-harper-fabric/.claude-plugin/plugin.json +1 -1
  44. package/plugins/lisa-harper-fabric/.codex-plugin/plugin.json +1 -1
  45. package/plugins/lisa-harper-fabric-agy/plugin.json +1 -1
  46. package/plugins/lisa-harper-fabric-copilot/.claude-plugin/plugin.json +1 -1
  47. package/plugins/lisa-harper-fabric-cursor/.claude-plugin/plugin.json +1 -1
  48. package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
  49. package/plugins/lisa-nestjs/.codex-plugin/plugin.json +1 -1
  50. package/plugins/lisa-nestjs-agy/plugin.json +1 -1
  51. package/plugins/lisa-nestjs-copilot/.claude-plugin/plugin.json +1 -1
  52. package/plugins/lisa-nestjs-cursor/.claude-plugin/plugin.json +1 -1
  53. package/plugins/lisa-openclaw/.claude-plugin/plugin.json +1 -1
  54. package/plugins/lisa-openclaw/.codex-plugin/plugin.json +1 -1
  55. package/plugins/lisa-openclaw-agy/plugin.json +1 -1
  56. package/plugins/lisa-openclaw-copilot/.claude-plugin/plugin.json +1 -1
  57. package/plugins/lisa-openclaw-cursor/.claude-plugin/plugin.json +1 -1
  58. package/plugins/lisa-phaser/.claude-plugin/plugin.json +1 -1
  59. package/plugins/lisa-phaser/.codex-plugin/plugin.json +1 -1
  60. package/plugins/lisa-phaser-agy/plugin.json +1 -1
  61. package/plugins/lisa-phaser-copilot/.claude-plugin/plugin.json +1 -1
  62. package/plugins/lisa-phaser-cursor/.claude-plugin/plugin.json +1 -1
  63. package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
  64. package/plugins/lisa-rails/.codex-plugin/plugin.json +1 -1
  65. package/plugins/lisa-rails-agy/plugin.json +1 -1
  66. package/plugins/lisa-rails-copilot/.claude-plugin/plugin.json +1 -1
  67. package/plugins/lisa-rails-cursor/.claude-plugin/plugin.json +1 -1
  68. package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
  69. package/plugins/lisa-typescript/.codex-plugin/plugin.json +1 -1
  70. package/plugins/lisa-typescript-agy/plugin.json +1 -1
  71. package/plugins/lisa-typescript-copilot/.claude-plugin/plugin.json +1 -1
  72. package/plugins/lisa-typescript-cursor/.claude-plugin/plugin.json +1 -1
  73. package/plugins/lisa-wiki/.claude-plugin/plugin.json +1 -1
  74. package/plugins/lisa-wiki/.codex-plugin/plugin.json +1 -1
  75. package/plugins/lisa-wiki-agy/plugin.json +1 -1
  76. package/plugins/lisa-wiki-copilot/.claude-plugin/plugin.json +1 -1
  77. package/plugins/lisa-wiki-cursor/.claude-plugin/plugin.json +1 -1
  78. package/plugins/src/base/skills/lisa-persist-learning/SKILL.md +32 -8
  79. package/scripts/generate-upstream-evidence-manifest.mjs +157 -0
@@ -1,170 +1,530 @@
1
- /** Allowlist-only body builder for public upstream Lisa attribution filings. */
2
- const INPUT_KEYS = [
3
- "markerKey",
4
- "failureClass",
1
+ /* eslint-disable max-lines, jsdoc/require-jsdoc, jsdoc/require-param-description, jsdoc/require-returns, functional/no-let, sonarjs/no-duplicate-string, code-organization/enforce-statement-order -- this security boundary is easier to audit as one ordered module */
2
+ /** Hash-pinned allowlist projection for public upstream Lisa filings. */
3
+ import { createHash } from "node:crypto";
4
+ import { closeSync, constants, fstatSync, openSync, readSync, realpathSync, } from "node:fs";
5
+ import path from "node:path";
6
+ import { isProxy } from "node:util/types";
7
+ import { fileURLToPath } from "node:url";
8
+ import { UPSTREAM_EVIDENCE_MANIFEST, UPSTREAM_PUBLIC_COMMITS, UPSTREAM_SURFACE_MANIFEST, } from "./upstream-evidence-manifest.js";
9
+ const PACKAGE_ROOT = realpathSync(path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../.."));
10
+ const MAX_LIST_ITEMS = 12;
11
+ const MAX_SOURCE_BYTES = 1_000_000;
12
+ const MAX_EXCERPT_BYTES = 16_384;
13
+ const MAX_AGGREGATE_EXCERPT_BYTES = 48_000;
14
+ const MAX_PUBLIC_BODY_BYTES = 60_000;
15
+ const MAX_PACKAGE_JSON_BYTES = 64_000;
16
+ const PACKAGE_PATH_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._/-]*$/;
17
+ const SURFACE_PATH_PATTERN = /^[A-Za-z0-9._-]+(?:\/[A-Za-z0-9._-]+)*$/;
18
+ const SHA_PATTERN = /^[a-f0-9]{40}$/;
19
+ const FINGERPRINT_PATTERN = /^sll4-[a-f0-9]{12}$/;
20
+ const TOP_LEVEL_FIELDS = Object.freeze([
21
+ "documentKind",
5
22
  "lisaSurface",
6
- "operatorImpact",
7
- "harnessFault",
8
- "requestedChange",
9
- "affectedProject",
10
- "hostIssueUrl",
11
- "attributionEvidence",
23
+ "failureClass",
12
24
  "lisaOwnedExcerpts",
13
- "upstreamRefs",
25
+ "upstreamCommitRefs",
26
+ "redactedPlaceholders",
27
+ "occurrenceFingerprint",
28
+ ]);
29
+ const EXCERPT_FIELDS = Object.freeze(["file", "text"]);
30
+ /** Closed, host-agnostic failure categories accepted in public filings. */
31
+ export const UPSTREAM_FAILURE_CLASSES = Object.freeze([
32
+ "access-control-failure",
33
+ "agent-parity-regression",
34
+ "configuration-regression",
35
+ "data-integrity-failure",
36
+ "dependency-regression",
37
+ "generated-artifact-regression",
38
+ "installation-regression",
39
+ "observability-gap",
40
+ "pagination-truncation",
41
+ "performance-regression",
42
+ "public-data-exposure",
43
+ "release-regression",
44
+ "runtime-regression",
45
+ "stale-artifact-overwrite",
46
+ "test-coverage-gap",
47
+ "validation-gap",
48
+ "workflow-contract-violation",
49
+ ]);
50
+ const FAILURE_CLASS_SET = new Set(UPSTREAM_FAILURE_CLASSES);
51
+ /** Fixed redaction tokens that can appear in public reproduction steps. */
52
+ export const UPSTREAM_REDACTED_PLACEHOLDERS = Object.freeze([
53
+ "<host-project>",
54
+ "<env-value>",
55
+ "<credential>",
56
+ "<connection-string>",
57
+ "<pii>",
58
+ "<host-payload>",
59
+ "<path>",
60
+ "<identifier>",
61
+ ]);
62
+ const PLACEHOLDER_SET = new Set(UPSTREAM_REDACTED_PLACEHOLDERS);
63
+ const PUBLIC_BODY_DENY_LIST = [
64
+ /\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}\b/i,
65
+ /\bAKIA[0-9A-Z]{16}\b/,
66
+ /\bgh[pousr]_\w{20,}\b/,
67
+ /\bxox[baprs]-[\w-]{10,}\b/,
68
+ /\b(?:api[_-]?key|access[_-]?key|password|passwd|secret|token|credential)\s*=\s*[\w+./=-]{16,}/i,
69
+ /-----BEGIN (?:RSA |EC |OPENSSH )?PRIVATE KEY-----/,
14
70
  ];
15
- const EXCERPT_KEYS = ["surface", "text"];
16
- const SIMPLE_BLOCKED_PATTERNS = [
17
- /AKIA[\dA-Z]{16}/u,
18
- /gh[pousr]_\w{20,}/u,
19
- /xox[baprs]-[\w-]{20,}/u,
20
- ];
21
- const isPlainRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
22
- const assertKnownKeys = (value, allowed, label) => {
23
- const unknownKeys = Object.keys(value).filter(key => !allowed.includes(key));
24
- if (unknownKeys.length > 0) {
25
- throw new Error(`${label} contains non-allowlisted field(s): ${unknownKeys.join(", ")}`);
26
- }
27
- };
28
- const requireString = (value, key) => {
29
- const field = value[key];
30
- if (typeof field !== "string" || field.trim().length === 0) {
31
- throw new Error(`upstream attribution field ${key} must be a non-empty string`);
32
- }
33
- if (matchesBlockedShape(field)) {
34
- throw new Error(`upstream attribution field ${key} matched a blocked shape`);
35
- }
36
- return field.trim();
37
- };
38
- const requireStringArray = (value, key) => {
39
- const field = value[key];
40
- if (!Array.isArray(field) || field.length === 0) {
41
- throw new Error(`upstream attribution field ${key} must be a non-empty array`);
42
- }
43
- return field.map((item, index) => {
44
- if (typeof item !== "string" || item.trim().length === 0) {
45
- throw new Error(`upstream attribution field ${key}[${index}] must be a non-empty string`);
46
- }
47
- if (matchesBlockedShape(item)) {
48
- throw new Error(`upstream attribution field ${key}[${index}] matched a blocked shape`);
49
- }
50
- return item.trim();
51
- });
52
- };
53
- const requireExcerpts = (value) => {
54
- const field = value.lisaOwnedExcerpts;
55
- if (!Array.isArray(field) || field.length === 0) {
56
- throw new Error("upstream attribution field lisaOwnedExcerpts must be a non-empty array");
57
- }
58
- return field.map((item, index) => {
59
- if (!isPlainRecord(item)) {
60
- throw new Error(`upstream attribution field lisaOwnedExcerpts[${index}] must be an object`);
61
- }
62
- assertKnownKeys(item, EXCERPT_KEYS, `lisaOwnedExcerpts[${index}]`);
63
- const surface = requireExcerptString(item, "surface", index);
64
- const text = requireExcerptString(item, "text", index);
65
- return { surface, text };
66
- });
67
- };
68
- const requireExcerptString = (value, key, index) => {
69
- const field = value[key];
70
- if (typeof field !== "string" || field.trim().length === 0) {
71
- throw new Error(`upstream attribution field lisaOwnedExcerpts[${index}].${key} must be a non-empty string`);
72
- }
73
- if (matchesBlockedShape(field)) {
74
- throw new Error(`upstream attribution field lisaOwnedExcerpts[${index}].${key} matched a blocked shape`);
75
- }
76
- return field.trim();
77
- };
78
- const bulletList = (items) => items.map(item => `- ${item}`).join("\n");
79
- const excerptList = (items) => items
80
- .map(item => `- ${item.surface}\n\n \`\`\`text\n ${item.text}\n \`\`\``)
81
- .join("\n");
82
- const hasHighEntropyAssignment = (value) => value.split(/\s+/u).some(token => {
83
- const [_key, fieldValue] = token.split("=", 2);
84
- if (fieldValue === undefined || fieldValue.length < 16) {
85
- return false;
86
- }
87
- return /[A-Za-z]/u.test(fieldValue) && /\d/u.test(fieldValue);
88
- });
89
- const hasEmailLikeToken = (value) => value.split(/\s+/u).some(token => {
90
- const atIndex = token.indexOf("@");
91
- const dotIndex = token.lastIndexOf(".");
92
- return atIndex > 0 && dotIndex > atIndex + 1 && dotIndex < token.length - 1;
93
- });
94
- const matchesBlockedShape = (value) => SIMPLE_BLOCKED_PATTERNS.some(pattern => pattern.test(value)) ||
95
- hasEmailLikeToken(value) ||
96
- hasHighEntropyAssignment(value);
97
- const parseBodyInput = (input) => {
98
- if (!isPlainRecord(input)) {
99
- throw new Error("upstream attribution body input must be an object");
100
- }
101
- const unknownKeys = Object.keys(input).filter(key => !INPUT_KEYS.includes(key));
102
- if (unknownKeys.length > 0) {
103
- throw new Error(`upstream attribution body input contains non-allowlisted field(s): ${unknownKeys.join(", ")}`);
104
- }
105
- const markerKey = requireString(input, "markerKey");
106
- const failureClass = requireString(input, "failureClass");
107
- const lisaSurface = requireString(input, "lisaSurface");
108
- const operatorImpact = requireString(input, "operatorImpact");
109
- const harnessFault = requireString(input, "harnessFault");
110
- const requestedChange = requireString(input, "requestedChange");
111
- const affectedProject = requireString(input, "affectedProject");
112
- const hostIssueUrl = requireString(input, "hostIssueUrl");
113
- const attributionEvidence = requireStringArray(input, "attributionEvidence");
114
- const upstreamRefs = requireStringArray(input, "upstreamRefs");
115
- const lisaOwnedExcerpts = requireExcerpts(input);
116
- return {
117
- markerKey,
118
- failureClass,
119
- lisaSurface,
120
- operatorImpact,
121
- harnessFault,
122
- requestedChange,
123
- affectedProject,
124
- hostIssueUrl,
125
- attributionEvidence,
126
- lisaOwnedExcerpts,
127
- upstreamRefs,
128
- };
129
- };
130
- /**
131
- * Build the public upstream issue body from enumerated fields only.
132
- *
133
- * Unknown fields are rejected instead of ignored so callers cannot smuggle
134
- * host-project prose into a world-readable Lisa issue body.
135
- *
136
- * @param input - Candidate body data from the handoff-upstream flow
137
- * @returns Markdown body safe to pass to the upstream GitHub issue writer
138
- */
71
+ /** A field-named, value-sanitized public projection rejection. */
72
+ export class UpstreamAttributionRejection extends Error {
73
+ field;
74
+ constructor(field, detail) {
75
+ super(`upstream-attribution: field "${field}": ${detail}`);
76
+ this.name = "UpstreamAttributionRejection";
77
+ this.field = field;
78
+ }
79
+ }
139
80
  export function buildUpstreamAttributionIssueBody(input) {
140
- const bodyInput = parseBodyInput(input);
81
+ const filing = validateAttribution(input);
82
+ const rootCauseKey = `${filing.lisaSurface.toLowerCase()}#${filing.failureClass}`;
83
+ const body = filing.documentKind === "occurrence"
84
+ ? renderOccurrence(filing, rootCauseKey)
85
+ : renderIssue(filing, rootCauseKey);
86
+ assertPublicTextSafe(body);
87
+ return body;
88
+ }
89
+ function renderIssue(filing, rootCauseKey) {
90
+ return [
91
+ `<!-- [lisa-upstream-attribution] key=${rootCauseKey} -->`,
92
+ "",
93
+ "## What failed for the operator",
94
+ "",
95
+ `A Lisa-managed surface produced a \`${filing.failureClass}\` failure, so the automated pipeline did not complete as intended. Host-project details are omitted from this public filing.`,
96
+ "",
97
+ "## What the harness did wrong",
98
+ "",
99
+ `Lisa's \`${filing.lisaSurface}\` surface did not prevent the \`${filing.failureClass}\` failure class. The evidence below contains only hash-pinned Lisa-owned text and public-origin commits.`,
100
+ "",
101
+ "## What to change",
102
+ "",
103
+ `Harden \`${filing.lisaSurface}\` against \`${filing.failureClass}\` and add a regression check using only the fixed redacted placeholders below.`,
104
+ "",
105
+ renderEvidence(filing),
106
+ "",
107
+ "_Body composed by `lisa file-upstream`; do not edit before filing._",
108
+ "",
109
+ ].join("\n");
110
+ }
111
+ function renderOccurrence(filing, rootCauseKey) {
112
+ return [
113
+ `<!-- [lisa-upstream-attribution-occurrence] key=${filing.occurrenceFingerprint ?? ""} -->`,
114
+ "",
115
+ "## Repeat encounter",
116
+ "",
117
+ `Another host project encountered root-cause key \`${rootCauseKey}\`. Host-project details are omitted; this comment contains only hash-pinned Lisa-owned evidence.`,
118
+ "",
119
+ renderEvidence(filing),
120
+ "",
121
+ "_Comment composed by `lisa file-upstream`; do not edit before posting._",
122
+ "",
123
+ ].join("\n");
124
+ }
125
+ function renderEvidence(filing) {
126
+ const excerpts = filing.excerpts
127
+ .map(excerpt => `- \`${excerpt.file}\`:\n\n${indentAsCode(excerpt.text)}`)
128
+ .join("\n\n");
129
+ const references = filing.upstreamCommitRefs
130
+ .map(reference => `- \`${reference}\``)
131
+ .join("\n");
132
+ const placeholders = filing.redactedPlaceholders
133
+ .map(placeholder => `- \`${placeholder}\``)
134
+ .join("\n");
141
135
  return [
142
- `<!-- [lisa-upstream-attribution] key=${bodyInput.markerKey} -->`,
136
+ "## Evidence chain",
143
137
  "",
144
- "## Operator impact",
145
- bodyInput.operatorImpact,
138
+ `- Lisa surface: \`${filing.lisaSurface}\``,
139
+ `- Failure class: \`${filing.failureClass}\``,
140
+ "- Host-project issue: [OMITTED BY PUBLIC FILING POLICY]",
146
141
  "",
147
- "## Harness fault",
148
- bodyInput.harnessFault,
142
+ "### Lisa-owned excerpts",
149
143
  "",
150
- "## Requested Lisa change",
151
- bodyInput.requestedChange,
144
+ excerpts,
152
145
  "",
153
- "## Redacted evidence chain",
154
- `- Failure class: ${bodyInput.failureClass}`,
155
- `- Lisa surface: ${bodyInput.lisaSurface}`,
156
- `- Affected project: ${bodyInput.affectedProject}`,
157
- `- Host-project issue: ${bodyInput.hostIssueUrl}`,
158
- "- Reproduction: REDACTED host values only; see linked host issue for private context.",
146
+ "### Public-origin commit references",
159
147
  "",
160
- "## Attribution evidence",
161
- bulletList(bodyInput.attributionEvidence),
148
+ references,
162
149
  "",
163
- "## Lisa-owned excerpts",
164
- excerptList(bodyInput.lisaOwnedExcerpts),
150
+ "### Redacted reproduction placeholders",
165
151
  "",
166
- "## Upstream references",
167
- bulletList(bodyInput.upstreamRefs),
152
+ placeholders,
168
153
  ].join("\n");
169
154
  }
155
+ function validateAttribution(input) {
156
+ assertRuntimeContract();
157
+ const record = requirePlainRecord(input, "input");
158
+ rejectUnknownFields(record, TOP_LEVEL_FIELDS, "input");
159
+ rejectAccessors(record, "input");
160
+ const documentKind = requireDocumentKind(readValue(record, "documentKind"));
161
+ const occurrenceFingerprint = requireFingerprintPairing(readOptionalValue(record, "occurrenceFingerprint"), documentKind);
162
+ const excerpts = requireExcerpts(readValue(record, "lisaOwnedExcerpts"));
163
+ const aggregateBytes = excerpts.reduce((total, excerpt) => total + Buffer.byteLength(excerpt.text, "utf8"), 0);
164
+ if (aggregateBytes > MAX_AGGREGATE_EXCERPT_BYTES) {
165
+ reject("lisaOwnedExcerpts", "exceeds the aggregate byte limit");
166
+ }
167
+ return {
168
+ documentKind,
169
+ lisaSurface: requireLisaSurface(readValue(record, "lisaSurface")),
170
+ failureClass: requireFailureClass(readValue(record, "failureClass")),
171
+ excerpts,
172
+ upstreamCommitRefs: requireCommitRefs(readValue(record, "upstreamCommitRefs")),
173
+ redactedPlaceholders: requirePlaceholders(readValue(record, "redactedPlaceholders")),
174
+ occurrenceFingerprint,
175
+ };
176
+ }
177
+ function assertRuntimeContract() {
178
+ if (Object.keys(UPSTREAM_EVIDENCE_MANIFEST).length === 0 ||
179
+ Object.keys(UPSTREAM_SURFACE_MANIFEST).length === 0 ||
180
+ Object.keys(UPSTREAM_PUBLIC_COMMITS).length === 0 ||
181
+ !Object.isFrozen(UPSTREAM_EVIDENCE_MANIFEST) ||
182
+ !Object.isFrozen(UPSTREAM_SURFACE_MANIFEST) ||
183
+ !Object.isFrozen(UPSTREAM_PUBLIC_COMMITS)) {
184
+ throw new Error("Upstream evidence manifests are missing or mutable");
185
+ }
186
+ const packageBytes = readBoundedRegularFile(path.join(PACKAGE_ROOT, "package.json"), MAX_PACKAGE_JSON_BYTES, "package identity");
187
+ let packageJson;
188
+ try {
189
+ packageJson = JSON.parse(decodeUtf8(packageBytes, "package identity"));
190
+ }
191
+ catch {
192
+ throw new Error("Invalid Lisa package identity");
193
+ }
194
+ if (packageJson === null ||
195
+ typeof packageJson !== "object" ||
196
+ isProxy(packageJson) ||
197
+ Object.getPrototypeOf(packageJson) !== Object.prototype ||
198
+ Object.getOwnPropertyDescriptor(packageJson, "name")?.value !==
199
+ "@codyswann/lisa") {
200
+ throw new Error("Invalid Lisa package identity");
201
+ }
202
+ }
203
+ function requirePlainRecord(value, field) {
204
+ if (value === null || typeof value !== "object") {
205
+ return reject(field, "must be a plain object");
206
+ }
207
+ if (isProxy(value)) {
208
+ return reject(field, "proxies are not allowed");
209
+ }
210
+ const prototype = Object.getPrototypeOf(value);
211
+ if (Array.isArray(value) || prototype !== Object.prototype) {
212
+ return reject(field, "must be a plain object");
213
+ }
214
+ return value;
215
+ }
216
+ function rejectUnknownFields(record, allowed, field) {
217
+ for (const key of Reflect.ownKeys(record)) {
218
+ if (typeof key !== "string") {
219
+ reject(field, "symbol fields are not allowlisted");
220
+ }
221
+ if (!allowed.includes(key)) {
222
+ reject(key, "is not allowlisted");
223
+ }
224
+ }
225
+ }
226
+ function rejectAccessors(record, field) {
227
+ for (const key of Object.keys(Object.getOwnPropertyDescriptors(record))) {
228
+ const descriptor = Object.getOwnPropertyDescriptor(record, key);
229
+ if (descriptor !== undefined && !("value" in descriptor)) {
230
+ reject(key || field, "accessors are not allowed");
231
+ }
232
+ }
233
+ }
234
+ function readValue(record, field) {
235
+ const descriptor = Object.getOwnPropertyDescriptor(record, field);
236
+ if (descriptor === undefined) {
237
+ return reject(field, "is required");
238
+ }
239
+ if (!("value" in descriptor)) {
240
+ return reject(field, "accessors are not allowed");
241
+ }
242
+ return descriptor.value;
243
+ }
244
+ function readOptionalValue(record, field) {
245
+ const descriptor = Object.getOwnPropertyDescriptor(record, field);
246
+ if (descriptor === undefined) {
247
+ return undefined;
248
+ }
249
+ if (!("value" in descriptor)) {
250
+ return reject(field, "accessors are not allowed");
251
+ }
252
+ return descriptor.value;
253
+ }
254
+ function requireDocumentKind(value) {
255
+ if (value !== "issue" && value !== "occurrence") {
256
+ return reject("documentKind", 'must be "issue" or "occurrence"');
257
+ }
258
+ return value;
259
+ }
260
+ function requireLisaSurface(value) {
261
+ const surface = requireAsciiString(value, "lisaSurface", 200);
262
+ assertDynamicTextSafe(surface, "lisaSurface");
263
+ if (!isSafeSurfacePath(surface) ||
264
+ !Object.hasOwn(UPSTREAM_SURFACE_MANIFEST, surface)) {
265
+ return reject("lisaSurface", "must be an exact public Lisa surface manifest member");
266
+ }
267
+ return surface;
268
+ }
269
+ function requireFailureClass(value) {
270
+ const failureClass = requireAsciiString(value, "failureClass", 120);
271
+ assertDynamicTextSafe(failureClass, "failureClass");
272
+ if (!FAILURE_CLASS_SET.has(failureClass)) {
273
+ return reject("failureClass", "is not in the closed failure taxonomy");
274
+ }
275
+ return failureClass;
276
+ }
277
+ function requireExcerpts(value) {
278
+ const entries = requireDenseArray(value, "lisaOwnedExcerpts", false);
279
+ return entries.map((entry, index) => {
280
+ const field = `lisaOwnedExcerpts[${index}]`;
281
+ const record = requirePlainRecord(entry, field);
282
+ rejectUnknownFields(record, EXCERPT_FIELDS, field);
283
+ rejectAccessors(record, field);
284
+ const file = requireManifestPath(readValue(record, "file"));
285
+ const text = requireExcerptText(readValue(record, "text"));
286
+ verifyExcerpt(file, text);
287
+ return { file, text };
288
+ });
289
+ }
290
+ function requireManifestPath(value) {
291
+ const member = requireAsciiString(value, "lisaOwnedExcerpts", 500);
292
+ assertDynamicTextSafe(member, "lisaOwnedExcerpts");
293
+ if (!isSafePackagePath(member) ||
294
+ !Object.hasOwn(UPSTREAM_EVIDENCE_MANIFEST, member)) {
295
+ return reject("lisaOwnedExcerpts", "file must be an exact public Lisa evidence manifest member");
296
+ }
297
+ return member;
298
+ }
299
+ function requireExcerptText(value) {
300
+ if (typeof value !== "string" || value.length === 0) {
301
+ return reject("lisaOwnedExcerpts", "text must be a non-empty string");
302
+ }
303
+ const bytes = Buffer.byteLength(value, "utf8");
304
+ if (bytes > MAX_EXCERPT_BYTES) {
305
+ return reject("lisaOwnedExcerpts", "text exceeds the per-excerpt byte limit");
306
+ }
307
+ if (Buffer.from(value, "utf8").toString("utf8") !== value) {
308
+ return reject("lisaOwnedExcerpts", "text must be valid UTF-8");
309
+ }
310
+ assertTextControlsSafe(value, "lisaOwnedExcerpts");
311
+ assertDynamicTextSafe(value, "lisaOwnedExcerpts");
312
+ if (value.includes("[lisa-upstream-attribution")) {
313
+ return reject("lisaOwnedExcerpts", "text must not contain a reserved upstream attribution marker");
314
+ }
315
+ return value;
316
+ }
317
+ function verifyExcerpt(file, excerpt) {
318
+ const absolutePath = path.join(PACKAGE_ROOT, file);
319
+ if (realpathOrReject(absolutePath) !== absolutePath) {
320
+ reject("lisaOwnedExcerpts", "file must not be a symlink");
321
+ }
322
+ const bytes = readBoundedRegularFile(absolutePath, MAX_SOURCE_BYTES, "lisaOwnedExcerpts");
323
+ const expectedHash = UPSTREAM_EVIDENCE_MANIFEST[file];
324
+ const actualHash = createHash("sha256").update(bytes).digest("hex");
325
+ if (expectedHash === undefined || actualHash !== expectedHash) {
326
+ reject("lisaOwnedExcerpts", "file failed its manifest hash check");
327
+ }
328
+ const contents = decodeUtf8(bytes, "lisaOwnedExcerpts");
329
+ assertTextControlsSafe(contents, "lisaOwnedExcerpts");
330
+ if (!contents.includes(excerpt)) {
331
+ reject("lisaOwnedExcerpts", "text is not present verbatim in its Lisa file");
332
+ }
333
+ }
334
+ function requireCommitRefs(value) {
335
+ return requireDenseArray(value, "upstreamCommitRefs", false).map(entry => {
336
+ const sha = requireAsciiString(entry, "upstreamCommitRefs", 40);
337
+ assertDynamicTextSafe(sha, "upstreamCommitRefs");
338
+ if (!SHA_PATTERN.test(sha) ||
339
+ !Object.hasOwn(UPSTREAM_PUBLIC_COMMITS, sha)) {
340
+ return reject("upstreamCommitRefs", "must contain full SHAs reachable from public origin refs");
341
+ }
342
+ return sha;
343
+ });
344
+ }
345
+ function requirePlaceholders(value) {
346
+ return requireDenseArray(value, "redactedPlaceholders", false).map(entry => {
347
+ const placeholder = requireAsciiString(entry, "redactedPlaceholders", 64);
348
+ assertDynamicTextSafe(placeholder, "redactedPlaceholders");
349
+ if (!PLACEHOLDER_SET.has(placeholder)) {
350
+ return reject("redactedPlaceholders", "is not in the fixed vocabulary");
351
+ }
352
+ return placeholder;
353
+ });
354
+ }
355
+ function requireDenseArray(value, field, allowEmpty) {
356
+ if (value === null || typeof value !== "object") {
357
+ return reject(field, "must be a plain array");
358
+ }
359
+ if (isProxy(value)) {
360
+ return reject(field, "proxies are not allowed");
361
+ }
362
+ if (!Array.isArray(value) ||
363
+ Object.getPrototypeOf(value) !== Array.prototype) {
364
+ return reject(field, "must be a plain array");
365
+ }
366
+ const length = requireArrayLength(value, field, allowEmpty);
367
+ assertArrayProperties(value, length, field);
368
+ return Array.from({ length }, (_unused, index) => {
369
+ const descriptor = Object.getOwnPropertyDescriptor(value, String(index));
370
+ if (descriptor === undefined || !("value" in descriptor)) {
371
+ return reject(field, "must be dense and accessor-free");
372
+ }
373
+ return descriptor.value;
374
+ });
375
+ }
376
+ function requireArrayLength(value, field, allowEmpty) {
377
+ const length = Object.getOwnPropertyDescriptor(value, "length")
378
+ ?.value;
379
+ if (typeof length !== "number" ||
380
+ !Number.isSafeInteger(length) ||
381
+ length > MAX_LIST_ITEMS ||
382
+ (!allowEmpty && length === 0)) {
383
+ return reject(field, `must contain 1-${MAX_LIST_ITEMS} items`);
384
+ }
385
+ return length;
386
+ }
387
+ function assertArrayProperties(value, length, field) {
388
+ const allowedIndices = new Set(Array.from({ length }, (_unused, index) => String(index)));
389
+ for (const key of Reflect.ownKeys(value)) {
390
+ if (typeof key !== "string" ||
391
+ (key !== "length" && !allowedIndices.has(key))) {
392
+ reject(field, "must not contain non-index properties");
393
+ }
394
+ const descriptor = Object.getOwnPropertyDescriptor(value, key);
395
+ if (descriptor === undefined || !("value" in descriptor)) {
396
+ reject(field, "must be dense and accessor-free");
397
+ }
398
+ }
399
+ }
400
+ function requireFingerprintPairing(value, documentKind) {
401
+ if (documentKind === "issue") {
402
+ return value === undefined
403
+ ? undefined
404
+ : reject("occurrenceFingerprint", "is valid only for occurrence events");
405
+ }
406
+ const fingerprint = requireAsciiString(value, "occurrenceFingerprint", 17);
407
+ assertDynamicTextSafe(fingerprint, "occurrenceFingerprint");
408
+ if (!FINGERPRINT_PATTERN.test(fingerprint)) {
409
+ return reject("occurrenceFingerprint", "must be sll4- plus exactly 12 lowercase hex characters");
410
+ }
411
+ return `sll4-${createHash("sha256")
412
+ .update(fingerprint, "utf8")
413
+ .digest("hex")
414
+ .slice(0, 12)}`;
415
+ }
416
+ function requireAsciiString(value, field, maxLength) {
417
+ if (typeof value !== "string" ||
418
+ value.length === 0 ||
419
+ value.length > maxLength ||
420
+ value.trim() !== value ||
421
+ !Array.from(value).every(character => {
422
+ const code = character.charCodeAt(0);
423
+ return code >= 0x20 && code <= 0x7e;
424
+ })) {
425
+ return reject(field, "must be bounded printable ASCII text");
426
+ }
427
+ return value;
428
+ }
429
+ function isSafePackagePath(value) {
430
+ return (PACKAGE_PATH_PATTERN.test(value) &&
431
+ !value.includes("//") &&
432
+ value.split("/").every(segment => segment !== "." && segment !== ".."));
433
+ }
434
+ function isSafeSurfacePath(value) {
435
+ return (SURFACE_PATH_PATTERN.test(value) &&
436
+ value.split("/").every(segment => segment !== "." && segment !== ".."));
437
+ }
438
+ function realpathOrReject(filePath) {
439
+ try {
440
+ return realpathSync(filePath);
441
+ }
442
+ catch {
443
+ return reject("lisaOwnedExcerpts", "file does not exist in this Lisa package");
444
+ }
445
+ }
446
+ function readBoundedRegularFile(filePath, maxBytes, field) {
447
+ const noFollow = constants.O_NOFOLLOW ?? 0;
448
+ let descriptor;
449
+ try {
450
+ descriptor = openSync(filePath, constants.O_RDONLY | constants.O_NONBLOCK | noFollow);
451
+ }
452
+ catch {
453
+ return reject(field, "must be a readable non-symlink file");
454
+ }
455
+ try {
456
+ const before = fstatSync(descriptor);
457
+ if (!before.isFile()) {
458
+ return reject(field, "must be a regular file");
459
+ }
460
+ if (before.size > maxBytes) {
461
+ return reject(field, "exceeds its source byte limit");
462
+ }
463
+ const bytes = Buffer.alloc(before.size);
464
+ const count = readFixedBytes(descriptor, bytes, 0);
465
+ const after = fstatSync(descriptor);
466
+ if (after.size !== before.size || count !== before.size) {
467
+ return reject(field, "changed while being read");
468
+ }
469
+ return bytes;
470
+ }
471
+ finally {
472
+ closeSync(descriptor);
473
+ }
474
+ }
475
+ function readFixedBytes(descriptor, buffer, offset) {
476
+ if (offset >= buffer.length) {
477
+ return offset;
478
+ }
479
+ const count = readSync(descriptor, buffer, offset, buffer.length - offset, offset);
480
+ return count === 0
481
+ ? offset
482
+ : readFixedBytes(descriptor, buffer, offset + count);
483
+ }
484
+ function decodeUtf8(bytes, field) {
485
+ const decoded = bytes.toString("utf8");
486
+ if (!Buffer.from(decoded, "utf8").equals(bytes)) {
487
+ return reject(field, "must be valid UTF-8 text");
488
+ }
489
+ return decoded;
490
+ }
491
+ function assertTextControlsSafe(value, field) {
492
+ const unsafe = Array.from(value).some(character => {
493
+ const code = character.codePointAt(0) ?? 0;
494
+ const c0 = code < 0x20 && code !== 0x09 && code !== 0x0a && code !== 0x0d;
495
+ const c1 = code >= 0x7f && code <= 0x9f;
496
+ const bidi = code === 0x061c ||
497
+ code === 0x200e ||
498
+ code === 0x200f ||
499
+ (code >= 0x202a && code <= 0x202e) ||
500
+ (code >= 0x2066 && code <= 0x2069);
501
+ return c0 || c1 || bidi;
502
+ });
503
+ if (unsafe) {
504
+ reject(field, "must not contain control or bidirectional override characters");
505
+ }
506
+ }
507
+ function indentAsCode(value) {
508
+ return value
509
+ .split("\n")
510
+ .map(line => ` ${line}`)
511
+ .join("\n");
512
+ }
513
+ function assertPublicTextSafe(value) {
514
+ if (Buffer.byteLength(value, "utf8") > MAX_PUBLIC_BODY_BYTES) {
515
+ reject("input", "projected document exceeds the public byte limit");
516
+ }
517
+ if (PUBLIC_BODY_DENY_LIST.some(pattern => pattern.test(value))) {
518
+ reject("input", "projected document contains a denied secret or PII shape");
519
+ }
520
+ }
521
+ function assertDynamicTextSafe(value, field) {
522
+ if (PUBLIC_BODY_DENY_LIST.some(pattern => pattern.test(value))) {
523
+ reject(field, "contains a denied secret or PII shape");
524
+ }
525
+ }
526
+ function reject(field, detail) {
527
+ throw new UpstreamAttributionRejection(field, detail);
528
+ }
529
+ /* eslint-enable max-lines, jsdoc/require-jsdoc, jsdoc/require-param-description, jsdoc/require-returns, functional/no-let, sonarjs/no-duplicate-string, code-organization/enforce-statement-order -- end cohesive security boundary */
170
530
  //# sourceMappingURL=upstream-attribution-body.js.map