@lunora/advisor 0.0.0 → 1.0.0-alpha.2

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 (43) hide show
  1. package/LICENSE.md +105 -0
  2. package/README.md +130 -9
  3. package/__assets__/package-og.svg +14 -0
  4. package/dist/index.d.mts +1451 -0
  5. package/dist/index.d.ts +1451 -0
  6. package/dist/index.mjs +79 -0
  7. package/dist/packem_shared/AE_METRIC_EVENTS-DexctYv6.mjs +85 -0
  8. package/dist/packem_shared/adminRouteWithoutGuard-UUGBkAjU.mjs +33 -0
  9. package/dist/packem_shared/authApiCallWithoutHeaders-BeJhCZaf.mjs +38 -0
  10. package/dist/packem_shared/circularFk-B2freHrP.mjs +84 -0
  11. package/dist/packem_shared/constraintValidator-Dr9Py3FD.mjs +186 -0
  12. package/dist/packem_shared/containerOversizedInstance-5U1VKPRM.mjs +36 -0
  13. package/dist/packem_shared/containerPublicInternet-CuNerJE5.mjs +30 -0
  14. package/dist/packem_shared/duplicateIndex-BOublMSt.mjs +57 -0
  15. package/dist/packem_shared/emptyIndex-BX8EuEY7.mjs +32 -0
  16. package/dist/packem_shared/filterWithoutIndex-BYVeJaSs.mjs +31 -0
  17. package/dist/packem_shared/finding-Dm_zvzS1.mjs +16 -0
  18. package/dist/packem_shared/fk-index-IUK1ukgs.mjs +7 -0
  19. package/dist/packem_shared/fromServerSchema-DinF1nph.mjs +50 -0
  20. package/dist/packem_shared/hardcodedSecret-W2pz1UZB.mjs +35 -0
  21. package/dist/packem_shared/helpers-DNCkMWZQ.mjs +4 -0
  22. package/dist/packem_shared/hotShard-Ir5D0B6J.mjs +48 -0
  23. package/dist/packem_shared/hyperdriveOutsideAction-BgZqX7Xg.mjs +30 -0
  24. package/dist/packem_shared/indexReferencesUnknownField-DH0_dbUY.mjs +36 -0
  25. package/dist/packem_shared/indexUtilization-B5DMQ3bI.mjs +45 -0
  26. package/dist/packem_shared/maskUncoveredPiiColumn-DjGIPG6M.mjs +61 -0
  27. package/dist/packem_shared/nondeterministicQueryMutation-GXES1fLp.mjs +35 -0
  28. package/dist/packem_shared/policyReferencesUnknownTable-DtaIEovd.mjs +38 -0
  29. package/dist/packem_shared/publicArgumentUsesAny-C71b2NCf.mjs +32 -0
  30. package/dist/packem_shared/publicMutationWithoutRatelimit-xBpJ6GWK.mjs +36 -0
  31. package/dist/packem_shared/r2sqlOutsideAction-CtqxvMuV.mjs +30 -0
  32. package/dist/packem_shared/relationReferencesUnknownField-YznyXt_7.mjs +54 -0
  33. package/dist/packem_shared/relationReferencesUnknownTable-DrorpKYe.mjs +33 -0
  34. package/dist/packem_shared/rlsUncoveredTable-CxEfZ5eZ.mjs +56 -0
  35. package/dist/packem_shared/sqlInjectionRisk-zwytYGLt.mjs +26 -0
  36. package/dist/packem_shared/tableWithoutInsert-CbbaYIP4.mjs +34 -0
  37. package/dist/packem_shared/unboundedStringArgument-DThg2-wt.mjs +32 -0
  38. package/dist/packem_shared/unindexedForeignKey-BgJbKyqK.mjs +45 -0
  39. package/dist/packem_shared/unindexedRelationTarget-D6eyj6Xx.mjs +53 -0
  40. package/dist/packem_shared/userCreatingMutationWithoutCaptcha-CH31YsUZ.mjs +42 -0
  41. package/dist/packem_shared/workflowUnknownTarget-Cdd7WhKQ.mjs +34 -0
  42. package/dist/packem_shared/workflowUnused-D0jHxdz9.mjs +38 -0
  43. package/package.json +40 -17
@@ -0,0 +1,56 @@
1
+ import { e as emit } from './finding-Dm_zvzS1.mjs';
2
+
3
+ const rlsUncoveredTable = {
4
+ categories: ["SECURITY"],
5
+ description: "A public procedure reads or writes a table that is covered by an RLS policy elsewhere in the app, but this procedure's builder chain does not include `.use(rls(...))`. The raw, unwrapped `ctx.db` silently bypasses every policy in the list.",
6
+ facing: "EXTERNAL",
7
+ level: "WARN",
8
+ name: "rls_uncovered_table",
9
+ remediation: "Add `.use(rls(policies))` to the procedure's builder chain — e.g. `c.use(rls(myPolicies)).query(...)` — so its `ctx.db` is wrapped by the same policy evaluator as the rest of the app. If this procedure is intentionally privileged (e.g. a background job), declare it with `internalQuery` / `internalMutation` / `internalAction` instead of the public builder so the intent is explicit.",
10
+ // eslint-disable-next-line sonarjs/cognitive-complexity -- the policy/table cross-reference is clearest as one linear pass; splitting it would obscure the flow.
11
+ run: (context) => {
12
+ if (context.rlsProcedures === void 0) {
13
+ return [];
14
+ }
15
+ const policyCoveredTables = /* @__PURE__ */ new Set();
16
+ for (const procedure of context.rlsProcedures) {
17
+ for (const table of procedure.rlsTables) {
18
+ if (table !== "") {
19
+ policyCoveredTables.add(table);
20
+ }
21
+ }
22
+ }
23
+ if (policyCoveredTables.size === 0) {
24
+ return [];
25
+ }
26
+ const findings = [];
27
+ for (const procedure of context.rlsProcedures) {
28
+ if (procedure.usesRls) {
29
+ continue;
30
+ }
31
+ if (procedure.visibility === "internal") {
32
+ continue;
33
+ }
34
+ const touched = /* @__PURE__ */ new Set();
35
+ for (const table of [...procedure.tablesRead, ...procedure.tablesWritten]) {
36
+ if (table !== "" && policyCoveredTables.has(table)) {
37
+ touched.add(table);
38
+ }
39
+ }
40
+ for (const table of touched) {
41
+ findings.push(
42
+ emit(rlsUncoveredTable, {
43
+ cacheKey: `rls_uncovered_table:${procedure.file}:${procedure.exportName}:${table}`,
44
+ detail: `\`${procedure.exportName}\` in ${procedure.file} accesses table \`${table}\` without \`.use(rls(...))\` — the table is policy-gated elsewhere in the app but this procedure bypasses those policies entirely.`,
45
+ metadata: { exportName: procedure.exportName, file: procedure.file, table }
46
+ })
47
+ );
48
+ }
49
+ }
50
+ return findings;
51
+ },
52
+ source: "static",
53
+ title: "RLS-gated table accessed without rls() middleware"
54
+ };
55
+
56
+ export { rlsUncoveredTable as default };
@@ -0,0 +1,26 @@
1
+ import { e as emit } from './finding-Dm_zvzS1.mjs';
2
+
3
+ const sqlInjectionRisk = {
4
+ categories: ["SECURITY"],
5
+ description: "A `ctx.sql` tagged-template interpolates an unparameterized string-building expression (concatenation or nested template) instead of a bound value — splicing raw text into the query and reopening SQL injection.",
6
+ facing: "EXTERNAL",
7
+ level: "ERROR",
8
+ name: "sql_injection_risk",
9
+ remediation: "Pass the value through a bound placeholder so the driver parameterizes it — keep request input inside a `ctx.sql` placeholder instead of concatenating it into the query string. Never build SQL text from request input by hand.",
10
+ run: (context) => {
11
+ if (context.sqlInterpolations === void 0) {
12
+ return [];
13
+ }
14
+ return context.sqlInterpolations.map(
15
+ (interpolation) => emit(sqlInjectionRisk, {
16
+ cacheKey: `sql_injection_risk:${interpolation.file}:${interpolation.line.toString()}`,
17
+ detail: `\`ctx.sql\` in \`${interpolation.exportName}\` (${interpolation.file}:${interpolation.line.toString()}) interpolates a string-building expression instead of a bound value — a SQL-injection vector. Pass the value through a bound placeholder so the driver parameterizes it.`,
18
+ metadata: { exportName: interpolation.exportName, file: interpolation.file, line: interpolation.line }
19
+ })
20
+ );
21
+ },
22
+ source: "static",
23
+ title: "Possible SQL injection in ctx.sql interpolation"
24
+ };
25
+
26
+ export { sqlInjectionRisk as default };
@@ -0,0 +1,34 @@
1
+ import { e as emit } from './finding-Dm_zvzS1.mjs';
2
+
3
+ const tableWithoutInsert = {
4
+ categories: ["SCHEMA"],
5
+ description: 'No function inserts into this table via `ctx.db.insert("<table>", …)`. It may be read-only by design (seeded by a migration, replicated, or written through a path the advisor can\'t see) — or it may be dead schema.',
6
+ facing: "INTERNAL",
7
+ level: "INFO",
8
+ name: "table_without_insert",
9
+ remediation: 'If the table should be writable, add a mutation that calls `ctx.db.insert("<table>", …)`. If it is read-only or seeded elsewhere, this advisory can be ignored.',
10
+ run: (context) => {
11
+ if (context.inserts === void 0) {
12
+ return [];
13
+ }
14
+ const insertedTables = new Set(context.inserts.filter((write) => write.table !== "").map((write) => write.table));
15
+ const findings = [];
16
+ for (const table of context.schema.tables) {
17
+ if (insertedTables.has(table.name) || table.externallyManaged === true) {
18
+ continue;
19
+ }
20
+ findings.push(
21
+ emit(tableWithoutInsert, {
22
+ cacheKey: `table_without_insert:${table.name}`,
23
+ detail: `No function calls \`ctx.db.insert("${table.name}", …)\` — table "${table.name}" has no discovered insert path.`,
24
+ metadata: { table: table.name }
25
+ })
26
+ );
27
+ }
28
+ return findings;
29
+ },
30
+ source: "static",
31
+ title: "Table has no insert path"
32
+ };
33
+
34
+ export { tableWithoutInsert as default };
@@ -0,0 +1,32 @@
1
+ import { e as emit } from './finding-Dm_zvzS1.mjs';
2
+
3
+ const unboundedStringArgument = {
4
+ categories: ["SECURITY"],
5
+ description: "A public `v.string()` argument has no maximum-length bound. An unbounded string lets a client submit arbitrarily large input — abusing storage and CPU on every request that processes it.",
6
+ facing: "EXTERNAL",
7
+ level: "INFO",
8
+ name: "unbounded_string_arg",
9
+ remediation: "Add a max-length bound via `.check(...)` / `.meta({ maxLength })` on the string validator (e.g. cap a name at 256, a body at a few KB). Size the cap to the field's real-world maximum.",
10
+ run: (context) => {
11
+ if (context.argValidators === void 0) {
12
+ return [];
13
+ }
14
+ const findings = [];
15
+ for (const procedure of context.argValidators) {
16
+ for (const argument of procedure.unboundedStringArgs) {
17
+ findings.push(
18
+ emit(unboundedStringArgument, {
19
+ cacheKey: `unbounded_string_arg:${procedure.file}:${procedure.exportName}:${argument}`,
20
+ detail: `Arg \`${argument}\` of public procedure \`${procedure.exportName}\` (${procedure.file}:${procedure.line.toString()}) is an unbounded \`v.string()\`. Add a max-length bound to cap payload size.`,
21
+ metadata: { argument, exportName: procedure.exportName, file: procedure.file, line: procedure.line }
22
+ })
23
+ );
24
+ }
25
+ }
26
+ return findings;
27
+ },
28
+ source: "static",
29
+ title: "Public string argument has no length bound"
30
+ };
31
+
32
+ export { unboundedStringArgument as default };
@@ -0,0 +1,45 @@
1
+ import { e as emit } from './finding-Dm_zvzS1.mjs';
2
+ import { l as leadingIndexedColumns, P as PRIMARY_KEY, s as suggestIndexName } from './fk-index-IUK1ukgs.mjs';
3
+
4
+ const unindexedForeignKey = {
5
+ categories: ["PERFORMANCE"],
6
+ description: "A foreign-key column declared by a `one` relation has no index leading with it. Reads that filter or join on the column full-scan the table, which gets linearly slower as rows accumulate.",
7
+ facing: "EXTERNAL",
8
+ level: "INFO",
9
+ name: "unindexed_foreign_key",
10
+ remediation: 'Add a secondary index leading with the FK column, e.g. `.index("byAuthorId", ["authorId"])`.',
11
+ run: (context) => {
12
+ const findings = [];
13
+ for (const table of context.schema.tables) {
14
+ const indexed = leadingIndexedColumns(table);
15
+ for (const relation of table.relations) {
16
+ if (relation.kind !== "one") {
17
+ continue;
18
+ }
19
+ const fkColumn = relation.field;
20
+ if (fkColumn === PRIMARY_KEY || indexed.has(fkColumn)) {
21
+ continue;
22
+ }
23
+ const suggestedIndex = suggestIndexName(fkColumn);
24
+ findings.push(
25
+ emit(unindexedForeignKey, {
26
+ cacheKey: `unindexed_foreign_key:${table.name}:${fkColumn}`,
27
+ detail: `Relation "${relation.name}" on table "${table.name}" references "${relation.table}" via column "${fkColumn}", which is not the leading column of any index. Reads filtering or joining on "${fkColumn}" full-scan "${table.name}".`,
28
+ metadata: {
29
+ fkColumn,
30
+ references: { column: relation.references, table: relation.table },
31
+ relation: relation.name,
32
+ suggestedIndex: { fields: [fkColumn], name: suggestedIndex },
33
+ table: table.name
34
+ }
35
+ })
36
+ );
37
+ }
38
+ }
39
+ return findings;
40
+ },
41
+ source: "static",
42
+ title: "Unindexed foreign key"
43
+ };
44
+
45
+ export { unindexedForeignKey as default };
@@ -0,0 +1,53 @@
1
+ import { e as emit } from './finding-Dm_zvzS1.mjs';
2
+ import { P as PRIMARY_KEY, l as leadingIndexedColumns, s as suggestIndexName } from './fk-index-IUK1ukgs.mjs';
3
+
4
+ const unindexedRelationTarget = {
5
+ categories: ["PERFORMANCE"],
6
+ description: "A `many` relation's foreign-key column on the target table has no index leading with it. Relation predicates (`some`/`none`/`every`) and `with:` child reads filter the target on that column, full-scanning it as rows accumulate.",
7
+ facing: "EXTERNAL",
8
+ level: "INFO",
9
+ name: "unindexed_relation_target",
10
+ remediation: 'Add a secondary index on the target table leading with the FK column, e.g. `.index("byAuthorId", ["authorId"])`.',
11
+ run: (context) => {
12
+ const findings = [];
13
+ const tablesByName = new Map(context.schema.tables.map((table) => [table.name, table]));
14
+ for (const table of context.schema.tables) {
15
+ for (const relation of table.relations) {
16
+ if (relation.kind !== "many") {
17
+ continue;
18
+ }
19
+ const target = tablesByName.get(relation.table);
20
+ if (target === void 0) {
21
+ continue;
22
+ }
23
+ const fkColumn = relation.field;
24
+ if (fkColumn === PRIMARY_KEY) {
25
+ continue;
26
+ }
27
+ const coveredByForeignKeyLint = target.relations.some((targetRelation) => targetRelation.kind === "one" && targetRelation.field === fkColumn);
28
+ if (coveredByForeignKeyLint || leadingIndexedColumns(target).has(fkColumn)) {
29
+ continue;
30
+ }
31
+ const suggestedIndex = suggestIndexName(fkColumn);
32
+ findings.push(
33
+ emit(unindexedRelationTarget, {
34
+ cacheKey: `unindexed_relation_target:${relation.table}:${fkColumn}`,
35
+ detail: `Relation "${relation.name}" on table "${table.name}" is a to-many over "${relation.table}" via its column "${fkColumn}", which is not the leading column of any index on "${relation.table}". Relation predicates (\`some\`/\`none\`/\`every\`) and \`with:\` reads of "${relation.name}" full-scan "${relation.table}".`,
36
+ metadata: {
37
+ fkColumn,
38
+ references: { column: relation.references, table: table.name },
39
+ relation: relation.name,
40
+ suggestedIndex: { fields: [fkColumn], name: suggestedIndex },
41
+ table: relation.table
42
+ }
43
+ })
44
+ );
45
+ }
46
+ }
47
+ return findings;
48
+ },
49
+ source: "static",
50
+ title: "Unindexed relation target"
51
+ };
52
+
53
+ export { unindexedRelationTarget as default };
@@ -0,0 +1,42 @@
1
+ import { e as emit } from './finding-Dm_zvzS1.mjs';
2
+
3
+ const userCreatingMutationWithoutCaptcha = {
4
+ categories: ["SECURITY"],
5
+ description: "A public `mutation`/`action` that creates a user/session or sends mail has no CAPTCHA / bot check. Account-creating and mail-sending endpoints are prime automated-abuse targets (credential stuffing, mailbox flooding, disposable-account farming).",
6
+ facing: "EXTERNAL",
7
+ level: "WARN",
8
+ name: "user_creating_mutation_without_captcha",
9
+ remediation: "Add a server-verified human check: `.use(verifyTurnstile({ secret, token }))` from `@lunora/auth`, or wrap it with `.use(protectPublic({ rateLimit, captcha }))` from `@lunora/server`. Pair with a rate limit for defense in depth.",
10
+ run: (context) => {
11
+ if (context.procedureProtections === void 0) {
12
+ return [];
13
+ }
14
+ const findings = [];
15
+ for (const procedure of context.procedureProtections) {
16
+ const isPublicWrite = procedure.visibility === "public" && (procedure.kind === "mutation" || procedure.kind === "action");
17
+ const sensitive = procedure.writesUserTable || procedure.callsMail;
18
+ if (!isPublicWrite || !sensitive || procedure.usesCaptcha) {
19
+ continue;
20
+ }
21
+ const reason = procedure.writesUserTable ? "writes a user/session table" : "sends mail";
22
+ findings.push(
23
+ emit(userCreatingMutationWithoutCaptcha, {
24
+ cacheKey: `user_creating_mutation_without_captcha:${procedure.file}:${procedure.exportName}`,
25
+ detail: `Public ${procedure.kind} \`${procedure.exportName}\` (${procedure.file}) ${reason} but has no CAPTCHA check. Add \`.use(verifyTurnstile(...))\` or \`.use(protectPublic({ captcha }))\`.`,
26
+ metadata: {
27
+ callsMail: procedure.callsMail,
28
+ exportName: procedure.exportName,
29
+ file: procedure.file,
30
+ kind: procedure.kind,
31
+ writesUserTable: procedure.writesUserTable
32
+ }
33
+ })
34
+ );
35
+ }
36
+ return findings;
37
+ },
38
+ source: "static",
39
+ title: "Account-creating / mail-sending write without a CAPTCHA"
40
+ };
41
+
42
+ export { userCreatingMutationWithoutCaptcha as default };
@@ -0,0 +1,34 @@
1
+ import { e as emit } from './finding-Dm_zvzS1.mjs';
2
+
3
+ const workflowUnknownTarget = {
4
+ categories: ["SCHEMA"],
5
+ description: 'A `ctx.workflows.get("<name>")` call references a workflow that is not declared by any `defineWorkflow` export in `lunora/workflows.ts`. The name is a typo or points at a removed/renamed workflow — the call throws at runtime.',
6
+ facing: "INTERNAL",
7
+ level: "ERROR",
8
+ name: "workflow_unknown_target",
9
+ remediation: 'Fix the workflow name in the `ctx.workflows.get("…")` call, or add the missing `defineWorkflow` export to `lunora/workflows.ts`.',
10
+ run: (context) => {
11
+ if (context.workflows === void 0 || context.workflowCalls === void 0) {
12
+ return [];
13
+ }
14
+ const declared = new Set(context.workflows.map((workflow) => workflow.exportName));
15
+ const findings = [];
16
+ for (const call of context.workflowCalls) {
17
+ if (call.workflow === "" || declared.has(call.workflow)) {
18
+ continue;
19
+ }
20
+ findings.push(
21
+ emit(workflowUnknownTarget, {
22
+ cacheKey: `workflow_unknown_target:${call.file}:${call.exportName}:${call.workflow}`,
23
+ detail: `\`ctx.workflows.get("${call.workflow}")\` in "${call.exportName}" (${call.file}) references workflow "${call.workflow}", which is not declared in lunora/workflows.ts.`,
24
+ metadata: { exportName: call.exportName, file: call.file, line: call.line, workflow: call.workflow }
25
+ })
26
+ );
27
+ }
28
+ return findings;
29
+ },
30
+ source: "static",
31
+ title: "Workflow call references unknown workflow"
32
+ };
33
+
34
+ export { workflowUnknownTarget as default };
@@ -0,0 +1,38 @@
1
+ import { e as emit } from './finding-Dm_zvzS1.mjs';
2
+
3
+ const workflowUnused = {
4
+ categories: ["SCHEMA"],
5
+ description: 'No function starts this workflow via `ctx.workflows.get("<name>")`. It may be triggered through a path the advisor can\'t see (the Cloudflare API, a wrangler invocation, a cross-service binding) — or it may be dead code that still deploys as a billable WorkflowEntrypoint.',
6
+ facing: "INTERNAL",
7
+ level: "INFO",
8
+ name: "workflow_unused",
9
+ remediation: 'If the workflow should be triggered in-app, start it from a mutation/action with `ctx.workflows.get("<name>").create({ params })`. If it is started externally or is no longer needed, this advisory can be ignored (or remove the `defineWorkflow` export).',
10
+ run: (context) => {
11
+ if (context.workflows === void 0) {
12
+ return [];
13
+ }
14
+ const calls = context.workflowCalls ?? [];
15
+ if (calls.some((call) => call.workflow === "")) {
16
+ return [];
17
+ }
18
+ const started = new Set(calls.map((call) => call.workflow));
19
+ const findings = [];
20
+ for (const workflow of context.workflows) {
21
+ if (started.has(workflow.exportName)) {
22
+ continue;
23
+ }
24
+ findings.push(
25
+ emit(workflowUnused, {
26
+ cacheKey: `workflow_unused:${workflow.exportName}`,
27
+ detail: `No function calls \`ctx.workflows.get("${workflow.exportName}")\` — workflow "${workflow.exportName}" is declared but never started in app code.`,
28
+ metadata: { workflow: workflow.exportName }
29
+ })
30
+ );
31
+ }
32
+ return findings;
33
+ },
34
+ source: "static",
35
+ title: "Workflow is never started"
36
+ };
37
+
38
+ export { workflowUnused as default };
package/package.json CHANGED
@@ -1,31 +1,54 @@
1
1
  {
2
2
  "name": "@lunora/advisor",
3
- "version": "0.0.0",
3
+ "version": "1.0.0-alpha.2",
4
4
  "description": "Schema & query lints (splinter-style advisors) for Lunora, feeding the Studio Advisors view",
5
- "license": "FSL-1.1-Apache-2.0",
5
+ "keywords": [
6
+ "advisor",
7
+ "cloudflare",
8
+ "durable-objects",
9
+ "lint",
10
+ "lunora",
11
+ "schema",
12
+ "splinter",
13
+ "workers"
14
+ ],
6
15
  "homepage": "https://lunora.sh",
16
+ "bugs": "https://github.com/anolilab/lunora/issues",
17
+ "license": "FSL-1.1-Apache-2.0",
18
+ "author": {
19
+ "name": "Daniel Bannert",
20
+ "email": "d.bannert@anolilab.de"
21
+ },
7
22
  "repository": {
8
23
  "type": "git",
9
24
  "url": "git+https://github.com/anolilab/lunora.git",
10
25
  "directory": "packages/advisor"
11
26
  },
12
- "bugs": {
13
- "url": "https://github.com/anolilab/lunora/issues"
14
- },
15
- "keywords": [
16
- "lunora",
17
- "cloudflare",
18
- "workers",
19
- "durable-objects",
20
- "lint",
21
- "advisor",
22
- "schema",
23
- "splinter"
27
+ "files": [
28
+ "dist",
29
+ "README.md",
30
+ "LICENSE.md",
31
+ "__assets__"
24
32
  ],
33
+ "type": "module",
34
+ "sideEffects": false,
35
+ "main": "./dist/index.mjs",
36
+ "module": "./dist/index.mjs",
37
+ "types": "./dist/index.d.ts",
38
+ "exports": {
39
+ ".": {
40
+ "types": "./dist/index.d.ts",
41
+ "import": "./dist/index.mjs"
42
+ },
43
+ "./package.json": "./package.json"
44
+ },
25
45
  "publishConfig": {
26
46
  "access": "public"
27
47
  },
28
- "files": [
29
- "README.md"
30
- ]
48
+ "dependencies": {
49
+ "@lunora/server": "1.0.0-alpha.1"
50
+ },
51
+ "engines": {
52
+ "node": "^22.15.0 || >=24.11.0"
53
+ }
31
54
  }