@lunora/advisor 1.0.0-alpha.2 → 1.0.0-alpha.20

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 (68) hide show
  1. package/LICENSE.md +6 -0
  2. package/README.md +1 -1
  3. package/__assets__/package-og.svg +1 -1
  4. package/dist/index.d.mts +2158 -4
  5. package/dist/index.d.ts +2158 -4
  6. package/dist/index.mjs +110 -8
  7. package/dist/packem_shared/{AE_METRIC_EVENTS-DexctYv6.mjs → AE_METRIC_EVENTS-BM14d0lm.mjs} +3 -1
  8. package/dist/packem_shared/actionFetchSsrf-wbmYgzmz.mjs +26 -0
  9. package/dist/packem_shared/aiRawRunEscapeHatch-C29jd32J.mjs +26 -0
  10. package/dist/packem_shared/aiToolSideEffectPromptInjection-BtD3alqB.mjs +33 -0
  11. package/dist/packem_shared/aiUnboundedGenerationPublic-CbAcSxLB.mjs +26 -0
  12. package/dist/packem_shared/allowUnauthenticatedShardAccessEnabled-BcASq-jP.mjs +30 -0
  13. package/dist/packem_shared/argument-derived-sink-C1xTTqAt.mjs +30 -0
  14. package/dist/packem_shared/authCsrfCheckDisabled-DCf9FSoD.mjs +26 -0
  15. package/dist/packem_shared/authEmailVerificationDisabled-Dk-Tc5oU.mjs +26 -0
  16. package/dist/packem_shared/authSecureCookiesDisabled-osJrHW9Y.mjs +26 -0
  17. package/dist/packem_shared/authSessionFreshageZero-CjufCc_Q.mjs +26 -0
  18. package/dist/packem_shared/authTrustedOriginsWildcard-ylv4PKzC.mjs +26 -0
  19. package/dist/packem_shared/browserAllowPrivateTargets-CBvQxLAM.mjs +26 -0
  20. package/dist/packem_shared/browserUserUrlWithoutAllowlist-CvhA6w59.mjs +26 -0
  21. package/dist/packem_shared/containerInstanceKeyFromUserInput-BUXj2J4w.mjs +19 -0
  22. package/dist/packem_shared/containerRuntimeEgressRelaxation-B0LlfmDA.mjs +26 -0
  23. package/dist/packem_shared/containerStartEnableInternetOverride-DDaHZQ1L.mjs +26 -0
  24. package/dist/packem_shared/externalSourceOnGlobal-Bg-NfCX9.mjs +30 -0
  25. package/dist/packem_shared/externalSourceUnscoped-5vT-Bup3.mjs +44 -0
  26. package/dist/packem_shared/flagGatesSecurityWithUnsafeDefault-BhIs0shr.mjs +41 -0
  27. package/dist/packem_shared/{fromServerSchema-DinF1nph.mjs → fromServerSchema-BiB6JSiW.mjs} +9 -0
  28. package/dist/packem_shared/helpers-BySnKhVB.mjs +31 -0
  29. package/dist/packem_shared/httpActionMissingAuthGuard-CxipddNx.mjs +35 -0
  30. package/dist/packem_shared/httpActionResponseHeaderInjection-DOFS7pFT.mjs +39 -0
  31. package/dist/packem_shared/identityUndeclaredClaimTrusted-D8nXV2dd.mjs +32 -0
  32. package/dist/packem_shared/imagesUrlSourceFromUserInput-YcDQ0b_Y.mjs +19 -0
  33. package/dist/packem_shared/{indexReferencesUnknownField-DH0_dbUY.mjs → indexReferencesUnknownField-BSWNngxX.mjs} +1 -1
  34. package/dist/packem_shared/insertManyUnsafeUserData-Dn77XpmX.mjs +26 -0
  35. package/dist/packem_shared/kvUnscopedUserKeyIdor-YWwmfE8X.mjs +19 -0
  36. package/dist/packem_shared/mailInboundDispatchWithoutVerify-CHSRA8zz.mjs +28 -0
  37. package/dist/packem_shared/mailRecipientFromRequestInput-Cka2qu5J.mjs +26 -0
  38. package/dist/packem_shared/maskWeakHashStrategyOnPii-1c4q8Opf.mjs +35 -0
  39. package/dist/packem_shared/maskedRelationLeakViaWith-CPI4s0sl.mjs +76 -0
  40. package/dist/packem_shared/mutatorFullRowReplace-BJnNDaIV.mjs +26 -0
  41. package/dist/packem_shared/normalizeIdUsedAsAuthorization-BVPtCpzT.mjs +50 -0
  42. package/dist/packem_shared/outputProjectionMissingOnPublicRead-Bl5IMx0k.mjs +51 -0
  43. package/dist/packem_shared/ownerFieldFromArgsNotAuth-mOw3hE5z.mjs +26 -0
  44. package/dist/packem_shared/paymentCreateWithoutAuthorize-BYm4JLxo.mjs +26 -0
  45. package/dist/packem_shared/paymentWebhookWideTolerance-D9QQJMJq.mjs +35 -0
  46. package/dist/packem_shared/plaintextSecretInWranglerVariables-NKO4YkKf.mjs +35 -0
  47. package/dist/packem_shared/privilegedDispatchUnvalidatedPayload-5Forjckt.mjs +37 -0
  48. package/dist/packem_shared/privilegedFanoutFromPublicProcedure-D3dL01B8.mjs +26 -0
  49. package/dist/packem_shared/{publicMutationWithoutRatelimit-xBpJ6GWK.mjs → publicMutationWithoutRatelimit-DbIhgi7j.mjs} +2 -2
  50. package/dist/packem_shared/publicTableRlsOptoutConfusion-Bn71yoD4.mjs +38 -0
  51. package/dist/packem_shared/queueWithoutDlq-CSkGNb_0.mjs +41 -0
  52. package/dist/packem_shared/ratelimitDefaultMemoryStore-BISChG5C.mjs +26 -0
  53. package/dist/packem_shared/ratelimitKeySpoofableOrGlobal-DqlHYQQ3.mjs +26 -0
  54. package/dist/packem_shared/ratelimitMiddlewareFailOpen-CJgDCaUw.mjs +33 -0
  55. package/dist/packem_shared/{relationReferencesUnknownField-YznyXt_7.mjs → relationReferencesUnknownField-CjbLScJ1.mjs} +1 -1
  56. package/dist/packem_shared/shapeTargetsGlobalTable-DHrf4Koi.mjs +34 -0
  57. package/dist/packem_shared/shapeUnknownTable-C8aDWFoe.mjs +34 -0
  58. package/dist/packem_shared/softDeleteIncludeDeletedFromArgs-BLqDKrkM.mjs +38 -0
  59. package/dist/packem_shared/storageGenerateUploadUrlNoContentTypePin-Da4L9Ge8.mjs +26 -0
  60. package/dist/packem_shared/storageKeyFromUserArgs-B86elJgS.mjs +19 -0
  61. package/dist/packem_shared/storagePresignedUrlForPrivateContent-yGmb8uqz.mjs +47 -0
  62. package/dist/packem_shared/storageUploadWithoutContentTypeAllowlist-BV-gF1lT.mjs +27 -0
  63. package/dist/packem_shared/storageUploadWithoutMaxSize-DpxO59wU.mjs +27 -0
  64. package/dist/packem_shared/{userCreatingMutationWithoutCaptcha-CH31YsUZ.mjs → userCreatingMutationWithoutCaptcha-2DZtWIPb.mjs} +2 -2
  65. package/dist/packem_shared/vectorsNamespaceFromUserInput-CQhr5bVn.mjs +19 -0
  66. package/dist/packem_shared/workflowDuplicateStepName-ioBxPBCy.mjs +48 -0
  67. package/package.json +4 -3
  68. package/dist/packem_shared/helpers-DNCkMWZQ.mjs +0 -4
@@ -0,0 +1,47 @@
1
+ import { e as emit } from './finding-Dm_zvzS1.mjs';
2
+
3
+ const SIGNER_METHODS = /* @__PURE__ */ new Set(["getPresignedUrl", "getSignedUrl"]);
4
+ const NEAR_MAX_EXPIRES_THRESHOLD_SECONDS = 6 * 24 * 60 * 60;
5
+ const isNativePresignedCall = (row) => row.method === "getPresignedUrl";
6
+ const isNearExpiryCeiling = (row) => SIGNER_METHODS.has(row.method) && row.expiresInSeconds !== void 0 && row.expiresInSeconds >= NEAR_MAX_EXPIRES_THRESHOLD_SECONDS;
7
+ const storagePresignedUrlForPrivateContent = {
8
+ categories: ["SECURITY"],
9
+ description: "`ctx.storage.getPresignedUrl(...)` mints a native S3 SigV4 URL that resolves directly against R2, bypassing the Worker's auth/RLS/rate-limit entirely — the wrong choice for private or policy-gated content. Either signer minting an `expiresInSeconds` near the shared 7-day ceiling also hands out a long-lived bearer credential.",
10
+ facing: "EXTERNAL",
11
+ level: "WARN",
12
+ name: "storage_presigned_url_for_private_content",
13
+ remediation: "For private/per-user/policy-gated content, use `getSignedUrl` (resolves back through the Worker, so your auth/RLS/rate-limit gates still run) instead of `getPresignedUrl`. Reserve `getPresignedUrl` for genuinely public or bulk content. On either signer, keep `expiresInSeconds` as short as the use case allows — well under the 7-day ceiling — rather than minting a near-maximum-lifetime URL.",
14
+ run: (context) => {
15
+ if (context.storageUploads === void 0) {
16
+ return [];
17
+ }
18
+ return context.storageUploads.filter((row) => isNativePresignedCall(row) || isNearExpiryCeiling(row)).map((row) => {
19
+ const native = isNativePresignedCall(row);
20
+ const nearCeiling = isNearExpiryCeiling(row);
21
+ const location = `\`${row.exportName}\` (${row.file}:${row.line.toString()})`;
22
+ let detail;
23
+ if (native && nearCeiling) {
24
+ detail = `\`ctx.storage.getPresignedUrl\` in ${location} mints a native S3 SigV4 URL — bypasses the Worker's auth/RLS/rate-limit entirely — with an \`expiresInSeconds\` of ${(row.expiresInSeconds ?? 0).toString()}s, near the 7-day signing ceiling.`;
25
+ } else if (native) {
26
+ detail = `\`ctx.storage.getPresignedUrl\` in ${location} mints a native S3 SigV4 URL that resolves directly against R2, bypassing the Worker's auth/RLS/rate-limit entirely. Use \`getSignedUrl\` for app-gated access to private content.`;
27
+ } else {
28
+ detail = `\`ctx.storage.${row.method}\` in ${location} requests an \`expiresInSeconds\` of ${(row.expiresInSeconds ?? 0).toString()}s, near the 7-day signing ceiling — a leaked link stays valid for almost a week.`;
29
+ }
30
+ return emit(storagePresignedUrlForPrivateContent, {
31
+ cacheKey: `storage_presigned_url_for_private_content:${row.file}:${row.line.toString()}`,
32
+ detail,
33
+ metadata: {
34
+ expiresInSeconds: row.expiresInSeconds,
35
+ exportName: row.exportName,
36
+ file: row.file,
37
+ line: row.line,
38
+ method: row.method
39
+ }
40
+ });
41
+ });
42
+ },
43
+ source: "static",
44
+ title: "Native presigned URL or near-max-TTL signed URL for private content"
45
+ };
46
+
47
+ export { storagePresignedUrlForPrivateContent as default };
@@ -0,0 +1,27 @@
1
+ import { e as emit } from './finding-Dm_zvzS1.mjs';
2
+
3
+ const UPLOAD_METHODS = /* @__PURE__ */ new Set(["store", "upload"]);
4
+ const storageUploadWithoutContentTypeAllowlist = {
5
+ categories: ["SECURITY"],
6
+ description: "A `ctx.storage.upload`/`store` call has no `allowedContentTypes` allowlist, so an uploader can store any content-type — including `text/html` or `image/svg+xml` — which, served back from `publicBaseUrl`, is a stored-XSS vector against your own origin.",
7
+ facing: "EXTERNAL",
8
+ level: "WARN",
9
+ name: "storage_upload_without_content_type_allowlist",
10
+ remediation: 'Pass `allowedContentTypes: ["image/png", "image/jpeg", …]` to `ctx.storage.upload`/`store` so an uploaded object\'s `contentType` must match an approved list. Reject anything HTML/SVG/script-executable unless the app genuinely needs to serve it.',
11
+ run: (context) => {
12
+ if (context.storageUploads === void 0) {
13
+ return [];
14
+ }
15
+ return context.storageUploads.filter((row) => UPLOAD_METHODS.has(row.method) && row.analyzable && !row.presentKeys.includes("allowedContentTypes")).map(
16
+ (row) => emit(storageUploadWithoutContentTypeAllowlist, {
17
+ cacheKey: `storage_upload_without_content_type_allowlist:${row.file}:${row.line.toString()}`,
18
+ detail: `\`ctx.storage.${row.method}\` in \`${row.exportName}\` (${row.file}:${row.line.toString()}) has no \`allowedContentTypes\` allowlist — any content-type is accepted, including \`text/html\`/\`image/svg+xml\`, a stored-XSS risk if the object is served from \`publicBaseUrl\`.`,
19
+ metadata: { exportName: row.exportName, file: row.file, line: row.line, method: row.method }
20
+ })
21
+ );
22
+ },
23
+ source: "static",
24
+ title: "Storage upload with no content-type allowlist"
25
+ };
26
+
27
+ export { storageUploadWithoutContentTypeAllowlist as default };
@@ -0,0 +1,27 @@
1
+ import { e as emit } from './finding-Dm_zvzS1.mjs';
2
+
3
+ const UPLOAD_METHODS = /* @__PURE__ */ new Set(["store", "upload"]);
4
+ const storageUploadWithoutMaxSize = {
5
+ categories: ["SECURITY"],
6
+ description: "A `ctx.storage.upload`/`store` call has no `maxSize` cap, so a caller can push an unbounded body straight into R2 through the Worker — unbounded storage/billing exhaustion, and for a streamed body, unbounded worker time.",
7
+ facing: "EXTERNAL",
8
+ level: "WARN",
9
+ name: "storage_upload_without_max_size",
10
+ remediation: "Pass a `maxSize` (bytes) to `ctx.storage.upload`/`store` sized to the largest legitimate object the app accepts. `ArrayBuffer`/`Blob` bodies are rejected up front when oversized; a `ReadableStream` body is aborted mid-transfer once the limit is exceeded.",
11
+ run: (context) => {
12
+ if (context.storageUploads === void 0) {
13
+ return [];
14
+ }
15
+ return context.storageUploads.filter((row) => UPLOAD_METHODS.has(row.method) && row.analyzable && !row.presentKeys.includes("maxSize")).map(
16
+ (row) => emit(storageUploadWithoutMaxSize, {
17
+ cacheKey: `storage_upload_without_max_size:${row.file}:${row.line.toString()}`,
18
+ detail: `\`ctx.storage.${row.method}\` in \`${row.exportName}\` (${row.file}:${row.line.toString()}) has no \`maxSize\` cap — a caller can push an unbounded body into R2, exhausting storage/billing.`,
19
+ metadata: { exportName: row.exportName, file: row.file, line: row.line, method: row.method }
20
+ })
21
+ );
22
+ },
23
+ source: "static",
24
+ title: "Storage upload with no size cap"
25
+ };
26
+
27
+ export { storageUploadWithoutMaxSize as default };
@@ -1,4 +1,5 @@
1
1
  import { e as emit } from './finding-Dm_zvzS1.mjs';
2
+ import { i as isPublicWrite } from './helpers-BySnKhVB.mjs';
2
3
 
3
4
  const userCreatingMutationWithoutCaptcha = {
4
5
  categories: ["SECURITY"],
@@ -13,9 +14,8 @@ const userCreatingMutationWithoutCaptcha = {
13
14
  }
14
15
  const findings = [];
15
16
  for (const procedure of context.procedureProtections) {
16
- const isPublicWrite = procedure.visibility === "public" && (procedure.kind === "mutation" || procedure.kind === "action");
17
17
  const sensitive = procedure.writesUserTable || procedure.callsMail;
18
- if (!isPublicWrite || !sensitive || procedure.usesCaptcha) {
18
+ if (!isPublicWrite(procedure) || !sensitive || procedure.usesCaptcha) {
19
19
  continue;
20
20
  }
21
21
  const reason = procedure.writesUserTable ? "writes a user/session table" : "sends mail";
@@ -0,0 +1,19 @@
1
+ import { m as makeArgumentDerivedSinkLint } from './argument-derived-sink-C1xTTqAt.mjs';
2
+
3
+ const vectorsNamespaceFromUserInput = makeArgumentDerivedSinkLint({
4
+ cacheKey: (access) => `vectors_namespace_from_user_input:${access.file}:${access.line.toString()}`,
5
+ categories: ["SECURITY"],
6
+ description: "A `ctx.vectors.query`/`upsert`/`upsertMany` call uses a `namespace` derived from the handler's `args` with no server-side scoping. A Vectorize namespace partitions one index into isolated sub-collections, so an unscoped namespace lets any caller read or poison another tenant's vectors.",
7
+ detail: (access) => `\`ctx.vectors.${access.method}\` in \`${access.exportName}\` (${access.file}:${access.line.toString()}) uses a Vectorize namespace derived from \`args\` with no server-side scoping — any caller can read or poison another tenant's vectors. Derive the namespace from a server-trusted identity (e.g. \`\${ctx.auth.orgId}\`), never from \`args\`.`,
8
+ facing: "EXTERNAL",
9
+ getAccesses: (context) => context.vectorNamespaceAccesses,
10
+ level: "WARN",
11
+ metadata: (access) => {
12
+ return { exportName: access.exportName, file: access.file, line: access.line, method: access.method };
13
+ },
14
+ name: "vectors_namespace_from_user_input",
15
+ remediation: `Derive the \`namespace\` from a server-trusted identity (e.g. \`\${ctx.auth.orgId}\`), never from \`args\`.`,
16
+ title: "Vectorize namespace derived from unscoped user input"
17
+ });
18
+
19
+ export { vectorsNamespaceFromUserInput as default };
@@ -0,0 +1,48 @@
1
+ import { e as emit } from './finding-Dm_zvzS1.mjs';
2
+
3
+ const workflowDuplicateStepName = {
4
+ categories: ["SCHEMA"],
5
+ description: "Two durable steps in this workflow share a name. Cloudflare memoizes a step by its name, so on replay the second call returns the first step's cached result instead of running its body — silently skipping the work without an error.",
6
+ facing: "INTERNAL",
7
+ level: "ERROR",
8
+ // eslint-disable-next-line no-secrets/no-secrets -- the lint's rule id, not a credential
9
+ name: "workflow_duplicate_step_name",
10
+ remediation: "Give every `step.do` / `step.sleep` / `step.sleepUntil` / `step.waitForEvent` call in the workflow a unique name. If a step legitimately repeats (e.g. a loop), make the name distinct per iteration by interpolating the item id into the step name.",
11
+ run: (context) => {
12
+ if (context.workflows === void 0) {
13
+ return [];
14
+ }
15
+ const findings = [];
16
+ for (const workflow of context.workflows) {
17
+ const { steps } = workflow;
18
+ if (steps === void 0 || steps.length === 0) {
19
+ continue;
20
+ }
21
+ const firstLineByName = /* @__PURE__ */ new Map();
22
+ const reported = /* @__PURE__ */ new Set();
23
+ for (const step of steps) {
24
+ const firstLine = firstLineByName.get(step.name);
25
+ if (firstLine === void 0) {
26
+ firstLineByName.set(step.name, step.line);
27
+ continue;
28
+ }
29
+ if (reported.has(step.name)) {
30
+ continue;
31
+ }
32
+ reported.add(step.name);
33
+ findings.push(
34
+ emit(workflowDuplicateStepName, {
35
+ cacheKey: `workflow_duplicate_step_name:${workflow.exportName}:${step.name}`,
36
+ detail: `Workflow "${workflow.exportName}" reuses the durable step name "${step.name}" (first at line ${String(firstLine)}, again at line ${String(step.line)}). The second call returns the first's cached result instead of running.`,
37
+ metadata: { firstLine, line: step.line, stepName: step.name, workflow: workflow.exportName }
38
+ })
39
+ );
40
+ }
41
+ }
42
+ return findings;
43
+ },
44
+ source: "static",
45
+ title: "Duplicate durable step name in workflow"
46
+ };
47
+
48
+ export { workflowDuplicateStepName as default };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lunora/advisor",
3
- "version": "1.0.0-alpha.2",
3
+ "version": "1.0.0-alpha.20",
4
4
  "description": "Schema & query lints (splinter-style advisors) for Lunora, feeding the Studio Advisors view",
5
5
  "keywords": [
6
6
  "advisor",
@@ -25,7 +25,7 @@
25
25
  "directory": "packages/advisor"
26
26
  },
27
27
  "files": [
28
- "dist",
28
+ "./dist",
29
29
  "README.md",
30
30
  "LICENSE.md",
31
31
  "__assets__"
@@ -46,7 +46,8 @@
46
46
  "access": "public"
47
47
  },
48
48
  "dependencies": {
49
- "@lunora/server": "1.0.0-alpha.1"
49
+ "@lunora/errors": "1.0.0-alpha.2",
50
+ "@lunora/server": "1.0.0-alpha.17"
50
51
  },
51
52
  "engines": {
52
53
  "node": "^22.15.0 || >=24.11.0"
@@ -1,4 +0,0 @@
1
- const SYSTEM_FIELDS = /* @__PURE__ */ new Set(["_creationTime", "_id"]);
2
- const tableColumnSet = (table) => /* @__PURE__ */ new Set([...SYSTEM_FIELDS, ...table.fields]);
3
-
4
- export { tableColumnSet as t };