@ihelpee/prumo-plugin-php-laravel 0.0.0-stage → 0.8.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/rules.js ADDED
@@ -0,0 +1,505 @@
1
+ import { rule, definePlugin, } from '@ihelpee/prumo/invariants';
2
+ import { extractPhpModel } from './ast.js';
3
+ /**
4
+ * Invariantes PHP/Laravel (spec `docs/especificacao-prumo-php-laravel.md` §3/§4).
5
+ * Os ids são canônicos e congelados; nenhum card filho pode renomeá-los sem
6
+ * voltar à especificação.
7
+ *
8
+ * Fronteiras honestas (Art. IV): cada regra declara o que NÃO pega.
9
+ * O modelo vem do subprocesso PHP (`needsData('php')`); a LEI está toda aqui.
10
+ */
11
+ const MUTATING = new Set(['POST', 'PUT', 'PATCH', 'DELETE']);
12
+ /**
13
+ * R2.2 — piso de símbolos que contam como proteção. `throttle:api` NÃO conta
14
+ * (rate-limit ≠ auth). Qualquer middleware cujo nome case com
15
+ * /auth|permission|role|guard/i conta. `plugin.authSymbols` (spec §5.2)
16
+ * ACRESCENTA símbolos do consumidor sobre este piso (match exato ou glob com
17
+ * `*`); nunca subtrai — o piso é o mínimo estrutural da regra.
18
+ */
19
+ const AUTH_SYMBOL_RE = /^(auth(:|$)|can(:|$)|verified$|EnsureEmailIsVerified$)/i;
20
+ const AUTH_HINT_RE = /auth|permission|role|guard/i;
21
+ function globToRe(pattern) {
22
+ const esc = pattern.replace(/[.*+?^${}()|[\]\\]/g, (c) => (c === '*' ? '.*' : '\\' + c));
23
+ return new RegExp(`^${esc}$`, 'i');
24
+ }
25
+ function isAuthSymbol(mw, extra) {
26
+ if (AUTH_SYMBOL_RE.test(mw) || AUTH_HINT_RE.test(mw))
27
+ return true;
28
+ return extra.some((s) => (s.includes('*') ? globToRe(s).test(mw) : s.toLowerCase() === mw.toLowerCase()));
29
+ }
30
+ /**
31
+ * R2.3 — exceção declarada: rota marcada com símbolo de `publicSymbols`
32
+ * (convenção opt-in do consumidor, ex.: `public`) é pública intencional.
33
+ */
34
+ function isPublicSymbol(mw, publicSymbols) {
35
+ return publicSymbols.some((s) => (s.includes('*') ? globToRe(s).test(mw) : s.toLowerCase() === mw.toLowerCase()));
36
+ }
37
+ /** Convenção Laravel: diretórios de oráculo quando o config não é visível. */
38
+ const DEFAULT_ORACLE_DIRS = ['tests/Oracle', 'tests/Feature/Oracle', 'oracle'];
39
+ function model(ctx) {
40
+ return (ctx.get('php') ?? null);
41
+ }
42
+ function isOracleFile(path, oracleDirs) {
43
+ const p = path.replace(/\\/g, '/');
44
+ return oracleDirs.some((d) => p === d || p.startsWith(d.replace(/\/+$/, '') + '/'));
45
+ }
46
+ /** `path` está sob algum prefixo de diretório (mesma semântica de isOracleFile). */
47
+ function isUnderAnyDir(path, dirs) {
48
+ const p = path.replace(/\\/g, '/');
49
+ return dirs.some((d) => p === d || p.startsWith(d.replace(/\/+$/, '') + '/'));
50
+ }
51
+ function controllersByName(m) {
52
+ const out = new Map();
53
+ for (const f of m.files) {
54
+ for (const c of f.controllers ?? []) {
55
+ if (!out.has(c.class))
56
+ out.set(c.class, c.middleware);
57
+ }
58
+ }
59
+ return out;
60
+ }
61
+ // ---------------------------------------------------------------------------
62
+ // F2 — Invariantes de aplicação
63
+ // ---------------------------------------------------------------------------
64
+ /**
65
+ * I1 / R2.1–R2.4 — rota de mutação (`POST/PUT/PATCH/DELETE`) sem middleware
66
+ * de autenticação/autorização em NENHUMA camada efetiva: middleware da rota,
67
+ * middleware do grupo aninhado, ou middleware do controller resolvido.
68
+ *
69
+ * R2.2/R2.3 (preset §5.2): `authSymbols` acrescenta símbolos de proteção ao
70
+ * piso estrutural; `publicSymbols` marca rota pública intencional (opt-in do
71
+ * consumidor) — sem convenção declarada, rota de mutação sem auth é sempre
72
+ * finding.
73
+ *
74
+ * Não pega: middleware global do stack web/api (exigiria ler
75
+ * bootstrap/app.php/Kernel — fronteira declarada; cubra via authSymbols no
76
+ * controller). Rotas GET nunca são alvo.
77
+ */
78
+ export function makeMutationRoutesGuarded(authSymbols, publicSymbols) {
79
+ return rule('mutation-routes-guarded')
80
+ .needsData('php')
81
+ .severity('error')
82
+ .message('rota de mutação Laravel sem middleware de auth em nenhuma camada (rota, grupo aninhado ou controller): em produção ela é pública, e actingAs() no teste forja a proteção que não existe')
83
+ .assert((ctx) => {
84
+ const m = model(ctx);
85
+ if (!m)
86
+ return [];
87
+ const out = [];
88
+ const ctrls = controllersByName(m);
89
+ for (const f of m.files) {
90
+ for (const r of f.routes ?? []) {
91
+ if (!MUTATING.has(r.method))
92
+ continue;
93
+ const ctrlMw = r.controller ? (ctrls.get(r.controller) ?? []) : [];
94
+ const layers = [...r.routeMiddleware, ...r.groupMiddleware, ...ctrlMw];
95
+ if (layers.some((mw) => isPublicSymbol(mw, publicSymbols)))
96
+ continue;
97
+ if (layers.some((mw) => isAuthSymbol(mw, authSymbols)))
98
+ continue;
99
+ const label = r.controller
100
+ ? `${r.controller}${r.handlerMethod ? '.' + r.handlerMethod : ''}`
101
+ : 'Closure';
102
+ out.push({
103
+ target: `${f.path}:${r.line} ${r.method} ${r.path} (${label})`,
104
+ location: `${f.path}:${r.line}`,
105
+ });
106
+ }
107
+ }
108
+ return out;
109
+ })
110
+ .build();
111
+ }
112
+ /**
113
+ * I2 / R2.5–R2.6 — método de controller com `Request` cru consumindo payload
114
+ * via all()/input()/json() sem validação inline; ou payload cru com
115
+ * `unguard()` no corpo (mass assignment forçado, mesmo com FormRequest).
116
+ *
117
+ * Não pega: `$request->validate([...])` inline (satisfaz, R2.8) e
118
+ * FormRequest tipado com rules() real.
119
+ */
120
+ export const formRequestValidated = rule('form-request-validated')
121
+ .needsData('php')
122
+ .severity('error')
123
+ .message('controller consome $request->all()/input()/json() cru sem FormRequest nem validate() — vetor clássico de mass assignment; use FormRequest com rules() ou $request->validate([...])')
124
+ .assert((ctx) => {
125
+ const m = model(ctx);
126
+ if (!m)
127
+ return [];
128
+ const out = [];
129
+ for (const f of m.files) {
130
+ for (const c of f.controllers ?? []) {
131
+ for (const meth of c.methods) {
132
+ const raw = meth.rawPayloadCalls.length > 0;
133
+ if (!raw)
134
+ continue;
135
+ const rawRequest = meth.requestParam === 'Request';
136
+ if ((rawRequest && !meth.validatesInline) || meth.unguard) {
137
+ out.push({
138
+ target: `${f.path}:${meth.line} ${c.class}.${meth.name} usa $request->${meth.rawPayloadCalls[0]}() sem validação${meth.unguard ? ' + unguard()' : ''}`,
139
+ location: `${f.path}:${meth.line}`,
140
+ });
141
+ }
142
+ }
143
+ }
144
+ }
145
+ return out;
146
+ })
147
+ .build();
148
+ /**
149
+ * R2.7 — FormRequest oco: classe gerada por `php artisan make:request` sem
150
+ * rules() preenchido (o agente faz isso constantemente) — autorização
151
+ * decorativa sobre rota que parece protegida. `warn`: a classe pode ser
152
+ * legítima em migração progressiva.
153
+ */
154
+ export const formRequestHollow = rule('form-request-hollow')
155
+ .needsData('php')
156
+ .severity('warn')
157
+ .message('FormRequest sem rules() preenchido: valida nada; o endpoint parece protegido por contrato e não é')
158
+ .assert((ctx) => {
159
+ const m = model(ctx);
160
+ if (!m)
161
+ return [];
162
+ const out = [];
163
+ for (const f of m.files) {
164
+ for (const fr of f.formRequests ?? []) {
165
+ if (!fr.hasRules) {
166
+ out.push({
167
+ target: `${f.path}:${fr.line} ${fr.class} sem rules()`,
168
+ location: `${f.path}:${fr.line}`,
169
+ });
170
+ }
171
+ }
172
+ }
173
+ return out;
174
+ })
175
+ .build();
176
+ /**
177
+ * I3 / R2.9–R2.11 — SQL bruto com interpolação/concatenação de variável é
178
+ * injeção; SQL bruto estático ou com bindings (`?`/named) é aceitável.
179
+ *
180
+ * False-positive guard (R2.11): interpolação de constante `self::`/`::CONST`
181
+ * e `config(...)` não é input externo — o extractor só marca T_VARIABLE, e
182
+ * constantes não são variáveis. Calibrado por fixture.
183
+ */
184
+ export const noRawQueries = rule('no-raw-queries')
185
+ .needsData('php')
186
+ .severity('error')
187
+ .message('SQL bruto interpolado ou sem bindings (DB::raw/whereRaw/selectRaw/DB::select...): injeção; use bindings (`?` / named placeholders)')
188
+ .assert((ctx) => {
189
+ const m = model(ctx);
190
+ if (!m)
191
+ return [];
192
+ const out = [];
193
+ for (const f of m.files) {
194
+ for (const q of f.rawQueries ?? []) {
195
+ const staticNoBindings = (q.call === 'DB::select' || q.call === 'DB::statement') && !q.hasBindings && !q.interpolated;
196
+ if (!q.interpolated && !staticNoBindings)
197
+ continue;
198
+ out.push({
199
+ target: `${f.path}:${q.line} ${q.call} ${q.interpolated ? 'interpolado' : 'sem bindings'}`,
200
+ location: `${f.path}:${q.line}`,
201
+ });
202
+ }
203
+ }
204
+ return out;
205
+ })
206
+ .build();
207
+ // ---------------------------------------------------------------------------
208
+ // F1 — Isolamento de oráculo em PHP
209
+ // ---------------------------------------------------------------------------
210
+ /**
211
+ * R3.1 — `use App\...` dentro de layout.oracle: o teste importa a unidade que
212
+ * deveria fiscalizar de fora. É espelho, não oráculo.
213
+ *
214
+ * Anti-burla (R3.3 / spec §6.4 / card F1): `app(Service::class)`,
215
+ * `resolve(...)`, `->make(...)`, `Container::make/get(...)` são IMPORT
216
+ * DINÂMICO — o teste que não escreveu `use App\...` ainda alcança a unidade.
217
+ * O extractor emite a aresta `serviceLocator` (método + arquivo inteiro);
218
+ * cada FQCN sob `App\` no oráculo é finding, exatamente como o `use` estático.
219
+ * Sem isto, o service-locator é a burla que vira a lei de cabeça para baixo.
220
+ */
221
+ export function makeOracleNoSrc(oracleDirs) {
222
+ return rule('oracle-no-src')
223
+ .needsData('php')
224
+ .severity('error')
225
+ .message('teste de oráculo importa App\\ (unidade interna) — por `use` estático OU por service-locator `app(FQCN::class)` (import dinâmico): é espelho, não oráculo; exercite a rota via kernel HTTP e derive a expectativa do contrato')
226
+ .assert((ctx) => {
227
+ const m = model(ctx);
228
+ if (!m)
229
+ return [];
230
+ const out = [];
231
+ for (const f of m.files) {
232
+ if (!isOracleFile(f.path, oracleDirs))
233
+ continue;
234
+ const seen = new Set();
235
+ // 1. `use App\...` estático (por método, como antes).
236
+ for (const t of f.tests ?? []) {
237
+ for (const meth of t.methods) {
238
+ for (const fqcn of meth.importsApp) {
239
+ if (seen.has(fqcn))
240
+ continue;
241
+ seen.add(fqcn);
242
+ out.push({ target: `${f.path} -> ${fqcn}`, location: f.path });
243
+ }
244
+ }
245
+ }
246
+ // 2. Anti-burla: service-locator com FQCN sob App\ (método + arquivo).
247
+ const locators = [
248
+ ...(f.serviceLocator ?? []),
249
+ ...(f.tests ?? []).flatMap((t) => t.methods.flatMap((meth) => meth.serviceLocator)),
250
+ ];
251
+ for (const loc of locators) {
252
+ if (!/^App(\\|$)/.test(loc.fqcn))
253
+ continue;
254
+ const key = `locator:${loc.fqcn}`;
255
+ if (seen.has(key))
256
+ continue;
257
+ seen.add(key);
258
+ out.push({
259
+ target: `${f.path}:${loc.line} -> ${loc.fqcn} (service-locator)`,
260
+ location: `${f.path}:${loc.line}`,
261
+ });
262
+ }
263
+ }
264
+ return out;
265
+ })
266
+ .build();
267
+ }
268
+ /**
269
+ * R3.2 — fronteira reversa: `app/**` importando `tests/**` (espelho indireto
270
+ * vazando para produção). O core resolve só imports TS; o extractor PHP dá a
271
+ * aresta (`imports[].fqcn`), e esta regra espelha a do core TS e a do plugin
272
+ * python (srcNoOracle): arquivo sob `srcDirs` cujo import resolve para um
273
+ * namespace de teste (`Tests\...`) é finding.
274
+ */
275
+ export function makeSrcNoOracle(srcDirs, testNamespaces) {
276
+ return rule('src-no-oracle')
277
+ .needsData('php')
278
+ .severity('error')
279
+ .message('implementação Laravel importa código de teste (use Tests\\...): fixture de teste vazando para produção')
280
+ .assert((ctx) => {
281
+ const m = model(ctx);
282
+ if (!m)
283
+ return [];
284
+ const out = [];
285
+ for (const f of m.files) {
286
+ if (!isUnderAnyDir(f.path, srcDirs))
287
+ continue;
288
+ for (const imp of f.imports ?? []) {
289
+ const ns = imp.fqcn.split('\\')[0] ?? '';
290
+ if (!testNamespaces.some((t) => t.toLowerCase() === ns.toLowerCase()))
291
+ continue;
292
+ out.push({
293
+ target: `${f.path}:${imp.line} -> ${imp.fqcn}`,
294
+ location: `${f.path}:${imp.line}`,
295
+ });
296
+ }
297
+ }
298
+ return out;
299
+ })
300
+ .build();
301
+ }
302
+ /**
303
+ * R3.3 / I4 — a regra-assinatura do plugin: dentro do oráculo,
304
+ * `withoutMiddleware()` remove e `actingAs()` forja exatamente a camada de
305
+ * autorização que I1 fiscaliza. O teste passa porque a proteção foi
306
+ * desligada, não porque a autorização funciona.
307
+ *
308
+ * Fronteira honesta: `actingAs` para obter token via fluxo real de login é
309
+ * aceitável (spec §4.2); o MVP marca o sinal e o alvo nomeia o método — o
310
+ * waiver existe para o caso legítimo, nunca o skip silencioso.
311
+ */
312
+ export function makeOracleNoAuthForge(oracleDirs) {
313
+ return rule('oracle-no-auth-forge')
314
+ .needsData('php')
315
+ .severity('error')
316
+ .message('oráculo forja/desliga autorização (withoutMiddleware/actingAs): mock circular da camada que a invariante fiscaliza; use credencial semeada + guard real')
317
+ .assert((ctx) => {
318
+ const m = model(ctx);
319
+ if (!m)
320
+ return [];
321
+ const out = [];
322
+ for (const f of m.files) {
323
+ if (!isOracleFile(f.path, oracleDirs))
324
+ continue;
325
+ for (const t of f.tests ?? []) {
326
+ for (const meth of t.methods) {
327
+ if (!meth.withoutMiddleware && !meth.actingAs)
328
+ continue;
329
+ const how = [
330
+ meth.withoutMiddleware ? 'withoutMiddleware()' : null,
331
+ meth.actingAs ? 'actingAs()' : null,
332
+ ].filter(Boolean).join(' + ');
333
+ out.push({
334
+ target: `${f.path}:${meth.line} ${t.class}.${meth.name} ${how}`,
335
+ location: `${f.path}:${meth.line}`,
336
+ });
337
+ }
338
+ }
339
+ }
340
+ return out;
341
+ })
342
+ .build();
343
+ }
344
+ /**
345
+ * R3.4 / I5 — fakes de container dentro do oráculo substituem unidades por
346
+ * crença do agente: espelho por construção.
347
+ */
348
+ export function makeOracleNoContainerFake(oracleDirs) {
349
+ return rule('oracle-no-container-fake')
350
+ .needsData('php')
351
+ .severity('error')
352
+ .message('oráculo usa fake/mock/swap de container (mock/partialMock/swap/fake/Container::set): expectativa derivada da crença sobre a unidade')
353
+ .assert((ctx) => {
354
+ const m = model(ctx);
355
+ if (!m)
356
+ return [];
357
+ const out = [];
358
+ for (const f of m.files) {
359
+ if (!isOracleFile(f.path, oracleDirs))
360
+ continue;
361
+ for (const t of f.tests ?? []) {
362
+ for (const meth of t.methods) {
363
+ if (meth.containerFakes.length === 0)
364
+ continue;
365
+ out.push({
366
+ target: `${f.path}:${meth.line} ${t.class}.${meth.name} ${meth.containerFakes.join('/')}`,
367
+ location: `${f.path}:${meth.line}`,
368
+ });
369
+ }
370
+ }
371
+ }
372
+ return out;
373
+ })
374
+ .build();
375
+ }
376
+ /**
377
+ * I6 / R3.5 — teste cujo corpo é só mock + assertCalled, sem asserção de
378
+ * domínio, é teste-espelho: verifica que o mock foi chamado, não que o
379
+ * negócio aconteceu. `warn` no MVP (heurística de corpo é frágil).
380
+ */
381
+ export const testMockOnly = rule('test-mock-only')
382
+ .needsData('php')
383
+ .severity('warn')
384
+ .message('teste só verifica que o mock foi chamado (expects/assertCalled) sem asserção de domínio: teste-espelho')
385
+ .assert((ctx) => {
386
+ const m = model(ctx);
387
+ if (!m)
388
+ return [];
389
+ const out = [];
390
+ for (const f of m.files) {
391
+ for (const t of f.tests ?? []) {
392
+ for (const meth of t.methods) {
393
+ if (!meth.mockOnly)
394
+ continue;
395
+ out.push({
396
+ target: `${f.path}:${meth.line} ${t.class}.${meth.name}`,
397
+ location: `${f.path}:${meth.line}`,
398
+ });
399
+ }
400
+ }
401
+ }
402
+ return out;
403
+ })
404
+ .build();
405
+ // ---------------------------------------------------------------------------
406
+ // Higiene do extractor (R1.3/R1.4) — nunca silencioso
407
+ // ---------------------------------------------------------------------------
408
+ /**
409
+ * R1.3 — arquivo que o tokenizador não leu (sintaxe ilegível) é finding
410
+ * `error`, nunca skip silencioso: gate que pula mudo reporta mais saúde do
411
+ * que existe.
412
+ */
413
+ export const phpParseError = rule('php-parse-error')
414
+ .needsData('php')
415
+ .severity('error')
416
+ .message('arquivo PHP não tokenizado pelo extractor (sintaxe ilegível): o gate não fiscalizou este arquivo')
417
+ .assert((ctx) => {
418
+ const m = model(ctx);
419
+ if (!m)
420
+ return [];
421
+ const out = [];
422
+ for (const f of m.files) {
423
+ if (!f.error)
424
+ continue;
425
+ out.push({ target: `${f.path} (${f.error})`, location: f.path });
426
+ }
427
+ return out;
428
+ })
429
+ .build();
430
+ /**
431
+ * R1.4 — `php` ausente no host: o extractor retornou null. O gate NÃO pode
432
+ * fingir que fiscalizou PHP: finding `warn` nomeia o runtime ausente e o
433
+ * `prumo doctor`/CI deve instalar o binário. Skip silencioso é a falsa
434
+ * sensação que a spec §8 lista como risco grave.
435
+ */
436
+ export const phpRuntimeMissing = rule('php-runtime-missing')
437
+ .needsData('php')
438
+ .severity('warn')
439
+ .message('binário `php` ausente no host: invariantes PHP/Laravel não rodaram; instale PHP >= 8.1 no CI (skip NÃO é silencioso)')
440
+ .assert((ctx) => {
441
+ const m = ctx.get('php');
442
+ if (m !== null)
443
+ return [];
444
+ return [{ target: 'php ausente no PATH' }];
445
+ })
446
+ .build();
447
+ // ---------------------------------------------------------------------------
448
+ // Plugin
449
+ // ---------------------------------------------------------------------------
450
+ function buildRules(opts) {
451
+ // As regras capturam a config via closure (factory): o RuleContext do core
452
+ // não expõe a config do consumidor, então as opções (spec §5.2) entram pelo
453
+ // factory `phpLaravel({ oracleDirs, authSymbols, publicSymbols, ... })`.
454
+ return [
455
+ makeMutationRoutesGuarded(opts.authSymbols, opts.publicSymbols),
456
+ formRequestValidated,
457
+ formRequestHollow,
458
+ noRawQueries,
459
+ makeOracleNoSrc(opts.oracleDirs),
460
+ makeSrcNoOracle(opts.srcDirs, opts.testNamespaces),
461
+ makeOracleNoAuthForge(opts.oracleDirs),
462
+ makeOracleNoContainerFake(opts.oracleDirs),
463
+ testMockOnly,
464
+ phpParseError,
465
+ phpRuntimeMissing,
466
+ ];
467
+ }
468
+ /**
469
+ * O plugin oficial PHP/Laravel do Prumo.
470
+ *
471
+ * Analisa, nunca executa: o subprocesso PHP só tokeniza (stdlib
472
+ * `token_get_all`); nenhuma linha do código analisado roda. `php` ausente →
473
+ * skip honesto com finding `php-runtime-missing`, nunca gate mudo.
474
+ */
475
+ export function phpLaravel(options = {}) {
476
+ const opts = {
477
+ oracleDirs: options.oracleDirs ?? DEFAULT_ORACLE_DIRS,
478
+ authSymbols: options.authSymbols ?? [],
479
+ publicSymbols: options.publicSymbols ?? [],
480
+ srcDirs: options.srcDirs ?? ['app'],
481
+ testNamespaces: options.testNamespaces ?? ['Tests'],
482
+ };
483
+ return definePlugin({
484
+ name: 'php-laravel',
485
+ version: '0.8.3',
486
+ description: 'PHP/Laravel invariants: guarded mutation routes, FormRequest discipline, parameterized SQL, oracle isolation (no auth forge, no container fakes, anti-burla service-locator).',
487
+ extractors: {
488
+ php: async (ctx) => {
489
+ try {
490
+ return await extractPhpModel(ctx.root);
491
+ }
492
+ catch (err) {
493
+ // timeout/JSON inválido: modelo vazio com erro marcado — as regras
494
+ // de app ficam mudas, php-parse-error fala (nunca silencioso).
495
+ const msg = err instanceof Error ? err.message : String(err);
496
+ return { version: 1, files: [{ path: '(extractor)', kind: 'other', error: msg }] };
497
+ }
498
+ },
499
+ },
500
+ rules: buildRules(opts),
501
+ });
502
+ }
503
+ /** Preset com a convenção padrão de oráculo (usa em invariants/rules.ts). */
504
+ export const phpLaravelPreset = phpLaravel().rules;
505
+ export default phpLaravel;
package/package.json CHANGED
@@ -1,6 +1,38 @@
1
1
  {
2
2
  "name": "@ihelpee/prumo-plugin-php-laravel",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "0.8.3",
4
+ "type": "module",
5
+ "description": "PHP/Laravel plugin for Prumo: guarded mutation routes, FormRequest discipline, parameterized SQL, and oracle isolation invariants.",
6
+ "author": "Alex Gotardi <alexgotardi@gmail.com> (https://github.com/alexgotardi)",
7
+ "main": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/index.d.ts",
12
+ "import": "./dist/index.js",
13
+ "require": "./dist/index.js"
14
+ }
15
+ },
16
+ "files": [
17
+ "dist",
18
+ "php",
19
+ "package.json",
20
+ "README.md"
21
+ ],
22
+ "sideEffects": false,
23
+ "license": "Apache-2.0",
24
+ "repository": {
25
+ "type": "git",
26
+ "url": "https://github.com/ihelpee/prumo.git"
27
+ },
28
+ "engines": {
29
+ "node": ">=20.11",
30
+ "php": ">=8.1"
31
+ },
32
+ "publishConfig": {
33
+ "access": "public"
34
+ },
35
+ "dependencies": {
36
+ "@ihelpee/prumo": "0.8.3"
37
+ }
6
38
  }