@kutur-kit/access 0.0.0-stage → 0.1.0-alpha.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 (48) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +224 -2
  3. package/dist/Condition.d.ts +76 -0
  4. package/dist/Condition.d.ts.map +1 -0
  5. package/dist/Condition.js +43 -0
  6. package/dist/Condition.js.map +1 -0
  7. package/dist/Decision.d.ts +39 -0
  8. package/dist/Decision.d.ts.map +1 -0
  9. package/dist/Decision.js +11 -0
  10. package/dist/Decision.js.map +1 -0
  11. package/dist/Permission.d.ts +82 -0
  12. package/dist/Permission.d.ts.map +1 -0
  13. package/dist/Permission.js +114 -0
  14. package/dist/Permission.js.map +1 -0
  15. package/dist/PermissionFilter.d.ts +25 -0
  16. package/dist/PermissionFilter.d.ts.map +1 -0
  17. package/dist/PermissionFilter.js +33 -0
  18. package/dist/PermissionFilter.js.map +1 -0
  19. package/dist/audit.d.ts +27 -0
  20. package/dist/audit.d.ts.map +1 -0
  21. package/dist/audit.js +46 -0
  22. package/dist/audit.js.map +1 -0
  23. package/dist/drizzle/postgres.d.ts +18 -0
  24. package/dist/drizzle/postgres.d.ts.map +1 -0
  25. package/dist/drizzle/postgres.js +65 -0
  26. package/dist/drizzle/postgres.js.map +1 -0
  27. package/dist/index.d.ts +5 -0
  28. package/dist/index.d.ts.map +1 -0
  29. package/dist/index.js +5 -0
  30. package/dist/index.js.map +1 -0
  31. package/dist/testing/PermissionTesting.d.ts +17 -0
  32. package/dist/testing/PermissionTesting.d.ts.map +1 -0
  33. package/dist/testing/PermissionTesting.js +19 -0
  34. package/dist/testing/PermissionTesting.js.map +1 -0
  35. package/dist/testing/index.d.ts +2 -0
  36. package/dist/testing/index.d.ts.map +1 -0
  37. package/dist/testing/index.js +2 -0
  38. package/dist/testing/index.js.map +1 -0
  39. package/package.json +88 -3
  40. package/src/Condition.ts +93 -0
  41. package/src/Decision.ts +45 -0
  42. package/src/Permission.ts +311 -0
  43. package/src/PermissionFilter.ts +93 -0
  44. package/src/audit.ts +82 -0
  45. package/src/drizzle/postgres.ts +108 -0
  46. package/src/index.ts +7 -0
  47. package/src/testing/PermissionTesting.ts +47 -0
  48. package/src/testing/index.ts +1 -0
@@ -0,0 +1,114 @@
1
+ import { Context, Effect, Layer, Predicate, Schema, SchemaTransformation } from "effect";
2
+ import * as Decision from "./Decision.js";
3
+ /**
4
+ * The keys of the permissions the caller may use, or undefined for all.
5
+ * Keys rather than ids, so permissions sharing an id stay apart.
6
+ */
7
+ export class Restriction extends Context.Reference("@kutur-kit/access/Permission/Restriction", { defaultValue: () => undefined }) {
8
+ }
9
+ // True while a check runs, so requires inside its policies only answer it too.
10
+ const Answering = Context.Reference("@kutur-kit/access/Permission/Answering", {
11
+ defaultValue: () => false,
12
+ });
13
+ // A WeakMap rather than a field keeps services from Layer.succeed valid.
14
+ const internals = new WeakMap();
15
+ const make = (internal) => {
16
+ const { key, id, decide } = internal;
17
+ const run = Effect.fnUntraced(function* (via, target) {
18
+ const answering = (yield* Answering) || via === "check";
19
+ const { decision } = yield* decide(answering ? "check" : via, target).pipe(Effect.provideService(Answering, answering));
20
+ yield* Effect.annotateCurrentSpan({
21
+ "access.permission": id,
22
+ "access.decision": decision._tag,
23
+ ...(Predicate.isTagged(decision, "Deny") && {
24
+ "access.denied_by": decision.by,
25
+ ...(decision.reason !== undefined && { "access.reason": decision.reason }),
26
+ ...(decision.requirement !== undefined && { "access.requirement": decision.requirement }),
27
+ }),
28
+ });
29
+ return decision;
30
+ });
31
+ const check = Effect.fn(`${key}.check`)(function* (...target) {
32
+ return yield* run("check", target);
33
+ });
34
+ const require = Effect.fn(`${key}.require`)(function* (...target) {
35
+ const decision = yield* run("require", target);
36
+ // A required permission's refusal fails as that permission's, as it would
37
+ // without this one, so callers can still tell which refused.
38
+ return yield* Predicate.isTagged(decision, "Deny")
39
+ ? Effect.fail(new Decision.Denied({ permission: decision.requirement ?? id }))
40
+ : Effect.void;
41
+ });
42
+ // SAFETY: Callers pass the arguments TargetArgs<D> describes, and Denied
43
+ // carries this permission's id.
44
+ const service = { check, require };
45
+ internals.set(service, internal);
46
+ return service;
47
+ };
48
+ /**
49
+ * Runs `f` after every decision of a permission service built by `toLayer`,
50
+ * inside the check's span and before `require` fails.
51
+ */
52
+ export const tap = (f) => (service) => {
53
+ // SAFETY: make stored the Internals of this service's I, D and R.
54
+ const internal = internals.get(service);
55
+ if (internal === undefined)
56
+ throw new Error("Permission.tap needs a permission service built by toLayer");
57
+ return make({
58
+ ...internal,
59
+ decide: (via, target) => Effect.tap(internal.decide(via, target), f),
60
+ });
61
+ };
62
+ /** Whether the caller's restriction excludes a permission, so it must refuse. */
63
+ export const isRestricted = (permission) => Effect.map(Restriction, (allowed) => allowed !== undefined && !allowed.has(permission.key));
64
+ export const restrictTo = (permissions) => (effect) => Effect.flatMap(Restriction, (current) => {
65
+ const allowed = Array.from(permissions, ({ key }) => key);
66
+ return Effect.provideService(effect, Restriction, new Set(current === undefined ? allowed : allowed.filter((key) => current.has(key))));
67
+ });
68
+ export const set = (permissions) => {
69
+ const ids = permissions.map((permission) => permission.id);
70
+ const duplicate = ids.find((id, index) => ids.indexOf(id) !== index);
71
+ if (duplicate !== undefined)
72
+ throw new Error(`Permission id ${duplicate} is used by more than one permission`);
73
+ // Ids name permissions outside the code, such as in stored token scopes.
74
+ const byId = new Map(permissions.map((permission) => [permission.id, permission]));
75
+ const isKnown = (id) => byId.has(id);
76
+ const schema = Schema.Array(Schema.String).pipe(Schema.decodeTo(Schema.Array(Schema.Literals(ids)), SchemaTransformation.transform({
77
+ decode: (stored) => stored.filter(isKnown),
78
+ encode: (decoded) => decoded,
79
+ })));
80
+ return {
81
+ schema,
82
+ restrictTo: (allowed) => restrictTo(Array.from(allowed).flatMap((id) => byId.get(id) ?? [])),
83
+ };
84
+ };
85
+ export const Service = () => (key, definition) => {
86
+ // SAFETY: IdOf<Key, D> is the declared id, or the key without one.
87
+ const id = (definition.id ?? key);
88
+ const Tag = Context.Service()(key);
89
+ const restricted = isRestricted({ id, key });
90
+ const toLayer = (policy) => Layer.effect(Tag, Effect.gen(function* () {
91
+ // SAFETY: Callers pass the arguments TargetArgs<D> describes.
92
+ const resolved = (Effect.isEffect(policy) ? yield* policy : policy);
93
+ const decide = Effect.fnUntraced(function* (via, target) {
94
+ const result = (yield* restricted) ? Decision.restricted() : resolved(...target);
95
+ const refusal = (error) => Schema.is(Decision.Denied)(error);
96
+ const answered = (answer) => answer.pipe(Effect.catchIf(refusal, (denied) => Effect.succeed(Decision.unmet(denied.permission))));
97
+ const decision = Effect.isEffect(result) ? yield* answered(result) : result;
98
+ // SAFETY: Callers pass the arguments TargetArgs<D> describes.
99
+ return { permission: id, via, target: target, decision };
100
+ });
101
+ return Tag.of(make({ key, id, decide }));
102
+ }));
103
+ const check = (...target) => Tag.use((permission) => permission.check(...target));
104
+ const require = (...target) => Tag.use((permission) => permission.require(...target));
105
+ return Object.assign(Tag, {
106
+ id,
107
+ definition,
108
+ Denied: Decision.deniedBy(id),
109
+ toLayer,
110
+ check,
111
+ require,
112
+ });
113
+ };
114
+ //# sourceMappingURL=Permission.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Permission.js","sourceRoot":"","sources":["../src/Permission.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,25 @@
1
+ import type { Scope } from "effect";
2
+ import { Context, Effect, Layer } from "effect";
3
+ import * as Condition from "./Condition.ts";
4
+ import * as Permission from "./Permission.ts";
5
+ export type Policy<D extends Permission.Definition, R> = (where: Condition.Builder<Permission.TargetOf<D>>) => Condition.Condition<Permission.TargetOf<D>> | Effect.Effect<Condition.Condition<Permission.TargetOf<D>>, Permission.CheckError<D>, R>;
6
+ export interface Api<D extends Permission.Definition, R> {
7
+ /** The targets the permission allows, or none outside the caller's restriction. */
8
+ readonly condition: Effect.Effect<Condition.Condition<Permission.TargetOf<D>>, Permission.CheckError<D>, R>;
9
+ }
10
+ export interface Statics<Self, D extends Permission.Definition, R> {
11
+ readonly condition: Effect.Effect<Condition.Condition<Permission.TargetOf<D>>, Permission.CheckError<D>, Self | R>;
12
+ readonly toLayer: <E = never, RL = never>(policy: Policy<D, R> | Effect.Effect<Policy<D, R>, E, RL>) => Layer.Layer<Self, E, Exclude<RL, Scope.Scope>>;
13
+ }
14
+ export type PermissionFilter<Self, D extends Permission.Definition, R> = Context.ServiceClass<Self, string, Api<D, R>> & Statics<Self, D, R>;
15
+ /**
16
+ * Defines the query form of a permission: the condition its policy allows,
17
+ * for listing targets in one query. Its key is the permission's with
18
+ * `/Filter`, so name the class after the permission too, such as
19
+ * `ViewInvoiceFilter`. Its policy must agree with the permission's;
20
+ * `PermissionTesting.assertFilterAgrees` checks that.
21
+ */
22
+ export declare const Service: <Self>() => <PermissionSelf, I extends string, D extends Permission.Definition & {
23
+ readonly target: Permission.Codec;
24
+ }, R>(permission: Permission.Tag<PermissionSelf, I, D, R>) => PermissionFilter<Self, D, R>;
25
+ //# sourceMappingURL=PermissionFilter.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"PermissionFilter.d.ts","sourceRoot":"","sources":["../src/PermissionFilter.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,QAAQ,CAAC;AAEpC,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,QAAQ,CAAC;AAEhD,OAAO,KAAK,SAAS,MAAM,gBAAgB,CAAC;AAC5C,OAAO,KAAK,UAAU,MAAM,iBAAiB,CAAC;AAE9C,MAAM,MAAM,MAAM,CAAC,CAAC,SAAS,UAAU,CAAC,UAAU,EAAE,CAAC,IAAI,CACvD,KAAK,EAAE,SAAS,CAAC,OAAO,CAAC,UAAU,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,KAE9C,SAAS,CAAC,SAAS,CAAC,UAAU,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,GAC3C,MAAM,CAAC,MAAM,CAAC,SAAS,CAAC,SAAS,CAAC,UAAU,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,EAAE,UAAU,CAAC,UAAU,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;AAE5F,MAAM,WAAW,GAAG,CAAC,CAAC,SAAS,UAAU,CAAC,UAAU,EAAE,CAAC;IACrD,mFAAmF;IACnF,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC,MAAM,CAC/B,SAAS,CAAC,SAAS,CAAC,UAAU,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,EAC3C,UAAU,CAAC,UAAU,CAAC,CAAC,CAAC,EACxB,CAAC,CACF,CAAC;CACH;AAED,MAAM,WAAW,OAAO,CAAC,IAAI,EAAE,CAAC,SAAS,UAAU,CAAC,UAAU,EAAE,CAAC;IAC/D,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC,MAAM,CAC/B,SAAS,CAAC,SAAS,CAAC,UAAU,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,EAC3C,UAAU,CAAC,UAAU,CAAC,CAAC,CAAC,EACxB,IAAI,GAAG,CAAC,CACT,CAAC;IACF,QAAQ,CAAC,OAAO,EAAE,CAAC,CAAC,GAAG,KAAK,EAAE,EAAE,GAAG,KAAK,EACtC,MAAM,EAAE,MAAM,CAAC,CAAC,EAAE,CAAC,CAAC,GAAG,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,KACtD,KAAK,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC,EAAE,OAAO,CAAC,EAAE,EAAE,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC;CACrD;AAED,MAAM,MAAM,gBAAgB,CAAC,IAAI,EAAE,CAAC,SAAS,UAAU,CAAC,UAAU,EAAE,CAAC,IAAI,OAAO,CAAC,YAAY,CAC3F,IAAI,EACJ,MAAM,EACN,GAAG,CAAC,CAAC,EAAE,CAAC,CAAC,CACV,GACC,OAAO,CAAC,IAAI,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC;AAEtB;;;;;;GAMG;AACH,eAAO,MAAM,OAAO,GACjB,IAAI,QAEH,cAAc,EACd,CAAC,SAAS,MAAM,EAChB,CAAC,SAAS,UAAU,CAAC,UAAU,GAAG;IAAE,QAAQ,CAAC,MAAM,EAAE,UAAU,CAAC,KAAK,CAAA;CAAE,EACvE,CAAC,cAEW,UAAU,CAAC,GAAG,CAAC,cAAc,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,KAClD,gBAAgB,CAAC,IAAI,EAAE,CAAC,EAAE,CAAC,CAoC7B,CAAC"}
@@ -0,0 +1,33 @@
1
+ import { Context, Effect, Layer } from "effect";
2
+ import * as Condition from "./Condition.js";
3
+ import * as Permission from "./Permission.js";
4
+ /**
5
+ * Defines the query form of a permission: the condition its policy allows,
6
+ * for listing targets in one query. Its key is the permission's with
7
+ * `/Filter`, so name the class after the permission too, such as
8
+ * `ViewInvoiceFilter`. Its policy must agree with the permission's;
9
+ * `PermissionTesting.assertFilterAgrees` checks that.
10
+ */
11
+ export const Service = () => (permission) => {
12
+ const key = `${permission.key}/Filter`;
13
+ const Tag = Context.Service()(key);
14
+ const toLayer = (policy) => Layer.effect(Tag, Effect.gen(function* () {
15
+ const resolved = Effect.isEffect(policy) ? yield* policy : policy;
16
+ const condition = Effect.gen(function* () {
17
+ const refused = yield* Permission.isRestricted(permission);
18
+ if (refused)
19
+ return Condition.make().none();
20
+ const result = resolved(Condition.make());
21
+ return Effect.isEffect(result) ? yield* result : result;
22
+ }).pipe(Effect.tap((result) => Effect.annotateCurrentSpan({
23
+ "access.permission": permission.id,
24
+ "access.condition": result._tag,
25
+ })), Effect.withSpan(`${key}.condition`));
26
+ return Tag.of({ condition });
27
+ }));
28
+ return Object.assign(Tag, {
29
+ condition: Tag.use((filter) => filter.condition),
30
+ toLayer,
31
+ });
32
+ };
33
+ //# sourceMappingURL=PermissionFilter.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"PermissionFilter.js","sourceRoot":"","sources":["../src/PermissionFilter.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,QAAQ,CAAC;AAEhD,OAAO,KAAK,SAAS,MAAM,gBAAgB,CAAC;AAC5C,OAAO,KAAK,UAAU,MAAM,iBAAiB,CAAC;AAmC9C;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,OAAO,GAClB,GAAS,EAAE,CACX,CAME,UAAmD,EACrB,EAAE;IAChC,MAAM,GAAG,GAAG,GAAG,UAAU,CAAC,GAAG,SAAS,CAAC;IACvC,MAAM,GAAG,GAAG,OAAO,CAAC,OAAO,EAAmB,CAAC,GAAG,CAAC,CAAC;IAEpD,MAAM,OAAO,GAAmC,CAAC,MAAM,EAAE,EAAE,CACzD,KAAK,CAAC,MAAM,CACV,GAAG,EACH,MAAM,CAAC,GAAG,CAAC,QAAQ,CAAC;QAClB,MAAM,QAAQ,GAAG,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC;QAElE,MAAM,SAAS,GAAG,MAAM,CAAC,GAAG,CAAC,QAAQ,CAAC;YACpC,MAAM,OAAO,GAAG,KAAK,CAAC,CAAC,UAAU,CAAC,YAAY,CAAC,UAAU,CAAC,CAAC;YAE3D,IAAI,OAAO;gBAAE,OAAO,SAAS,CAAC,IAAI,EAA0B,CAAC,IAAI,EAAE,CAAC;YAEpE,MAAM,MAAM,GAAG,QAAQ,CAAC,SAAS,CAAC,IAAI,EAAE,CAAC,CAAC;YAE1C,OAAO,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC;QAC1D,CAAC,CAAC,CAAC,IAAI,CACL,MAAM,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,EAAE,CACpB,MAAM,CAAC,mBAAmB,CAAC;YACzB,mBAAmB,EAAE,UAAU,CAAC,EAAE;YAClC,kBAAkB,EAAE,MAAM,CAAC,IAAI;SAChC,CAAC,CACH,EACD,MAAM,CAAC,QAAQ,CAAC,GAAG,GAAG,YAAY,CAAC,CACpC,CAAC;QAEF,OAAO,GAAG,CAAC,EAAE,CAAC,EAAE,SAAS,EAAE,CAAC,CAAC;IAC/B,CAAC,CAAC,CACH,CAAC;IAEJ,OAAO,MAAM,CAAC,MAAM,CAAC,GAAG,EAAE;QACxB,SAAS,EAAE,GAAG,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,SAAS,CAAC;QAChD,OAAO;KACR,CAAC,CAAC;AACL,CAAC,CAAC"}
@@ -0,0 +1,27 @@
1
+ import * as Audit from "@kutur-kit/audit/Audit";
2
+ import { Layer, Schema } from "effect";
3
+ import * as Permission from "./Permission.ts";
4
+ /** What refused a denied decision. Allowed decisions have no details. */
5
+ export declare const Details: Schema.UndefinedOr<Schema.Struct<{
6
+ readonly deniedBy: Schema.Literals<readonly ["restriction", "policy", "requirement"]>;
7
+ readonly requirement: Schema.optionalKey<Schema.String>;
8
+ }>>;
9
+ type CheckView<D extends Permission.Definition, V> = [V] extends [never] ? unknown : [V] extends [Permission.Codec] ? D extends {
10
+ readonly target: Permission.Codec;
11
+ } ? [Permission.TargetOf<D>] extends [V["Type"]] ? unknown : {
12
+ readonly view: Schema.Codec<Permission.TargetOf<D>, unknown>;
13
+ } : {
14
+ readonly view?: false;
15
+ } : unknown;
16
+ export interface Options<V> {
17
+ /** See `AuditEvent.Definition["view"]`. */
18
+ readonly view?: V;
19
+ }
20
+ /**
21
+ * Records the decisions of a permission's `require`s as audit events named by
22
+ * its id; `check`s only answer questions, so they are not recorded. Refusals
23
+ * fail the request whose transaction would store them, so writes are isolated.
24
+ */
25
+ export declare const record: <Self, I extends string, D extends Permission.Definition, R, const V extends Permission.Codec | false = never>(permission: Permission.Tag<Self, I, D, R>, options?: Options<V> & CheckView<D, V>) => <E, RL>(layer: Layer.Layer<Self, E, RL>) => Layer.Layer<Self, E, RL | Audit.AuditLog>;
26
+ export {};
27
+ //# sourceMappingURL=audit.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"audit.d.ts","sourceRoot":"","sources":["../src/audit.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,wBAAwB,CAAC;AAEhD,OAAO,EAAU,KAAK,EAAa,MAAM,EAAE,MAAM,QAAQ,CAAC;AAE1D,OAAO,KAAK,UAAU,MAAM,iBAAiB,CAAC;AAE9C,yEAAyE;AACzE,eAAO,MAAM,OAAO;;;GAKnB,CAAC;AAEF,KAAK,SAAS,CAAC,CAAC,SAAS,UAAU,CAAC,UAAU,EAAE,CAAC,IAAI,CAAC,CAAC,CAAC,SAAS,CAAC,KAAK,CAAC,GACpE,OAAO,GACP,CAAC,CAAC,CAAC,SAAS,CAAC,UAAU,CAAC,KAAK,CAAC,GAC5B,CAAC,SAAS;IAAE,QAAQ,CAAC,MAAM,EAAE,UAAU,CAAC,KAAK,CAAA;CAAE,GAC7C,CAAC,UAAU,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,GAC1C,OAAO,GACP;IAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC,KAAK,CAAC,UAAU,CAAC,QAAQ,CAAC,CAAC,CAAC,EAAE,OAAO,CAAC,CAAA;CAAE,GAClE;IAAE,QAAQ,CAAC,IAAI,CAAC,EAAE,KAAK,CAAA;CAAE,GAC3B,OAAO,CAAC;AAEd,MAAM,WAAW,OAAO,CAAC,CAAC;IACxB,2CAA2C;IAC3C,QAAQ,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;CACnB;AAED;;;;GAIG;AACH,eAAO,MAAM,MAAM,GACjB,IAAI,EACJ,CAAC,SAAS,MAAM,EAChB,CAAC,SAAS,UAAU,CAAC,UAAU,EAC/B,CAAC,EACD,KAAK,CAAC,CAAC,SAAS,UAAU,CAAC,KAAK,GAAG,KAAK,GAAG,KAAK,cAEpC,UAAU,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,YAC/B,OAAO,CAAC,CAAC,CAAC,GAAG,SAAS,CAAC,CAAC,EAAE,CAAC,CAAC,MAa9B,CAAC,EAAE,EAAE,SAAS,KAAK,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC,EAAE,EAAE,CAAC,KAAG,KAAK,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC,EAAE,EAAE,GAAG,KAAK,CAAC,QAAQ,CA0B1F,CAAC"}
package/dist/audit.js ADDED
@@ -0,0 +1,46 @@
1
+ import * as Audit from "@kutur-kit/audit/Audit";
2
+ import * as AuditEvent from "@kutur-kit/audit/AuditEvent";
3
+ import { Effect, Layer, Predicate, Schema } from "effect";
4
+ import * as Permission from "./Permission.js";
5
+ /** What refused a denied decision. Allowed decisions have no details. */
6
+ export const Details = Schema.UndefinedOr(Schema.Struct({
7
+ deniedBy: Schema.Literals(["restriction", "policy", "requirement"]),
8
+ requirement: Schema.optionalKey(Schema.String),
9
+ }));
10
+ /**
11
+ * Records the decisions of a permission's `require`s as audit events named by
12
+ * its id; `check`s only answer questions, so they are not recorded. Refusals
13
+ * fail the request whose transaction would store them, so writes are isolated.
14
+ */
15
+ export const record = (permission, options) => {
16
+ const view = options?.view;
17
+ // Permissions without a target log Schema.Undefined, which records nothing.
18
+ const event = AuditEvent.make(permission.id, {
19
+ target: permission.definition.target ?? Schema.Undefined,
20
+ ...(view !== undefined && { view }),
21
+ outcomes: ["Allow", "Deny"],
22
+ details: Details,
23
+ isolated: true,
24
+ });
25
+ return (layer) => Layer.effect(permission, Effect.gen(function* () {
26
+ const inner = yield* permission;
27
+ const auditLog = yield* Audit.AuditLog;
28
+ return Permission.tap(({ via, target: args, decision }) => {
29
+ if (via === "check")
30
+ return Effect.void;
31
+ const denied = Predicate.isTagged(decision, "Deny") ? decision : undefined;
32
+ return event
33
+ .log({
34
+ target: args[0],
35
+ outcome: decision._tag,
36
+ ...(denied?.reason !== undefined && { reason: denied.reason }),
37
+ details: denied && {
38
+ deniedBy: denied.by,
39
+ ...(denied.requirement !== undefined && { requirement: denied.requirement }),
40
+ },
41
+ })
42
+ .pipe(Effect.provideService(Audit.AuditLog, auditLog));
43
+ })(inner);
44
+ })).pipe(Layer.provide(layer));
45
+ };
46
+ //# sourceMappingURL=audit.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"audit.js","sourceRoot":"","sources":["../src/audit.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,wBAAwB,CAAC;AAChD,OAAO,KAAK,UAAU,MAAM,6BAA6B,CAAC;AAC1D,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAE1D,OAAO,KAAK,UAAU,MAAM,iBAAiB,CAAC;AAE9C,yEAAyE;AACzE,MAAM,CAAC,MAAM,OAAO,GAAG,MAAM,CAAC,WAAW,CACvC,MAAM,CAAC,MAAM,CAAC;IACZ,QAAQ,EAAE,MAAM,CAAC,QAAQ,CAAC,CAAC,aAAa,EAAE,QAAQ,EAAE,aAAa,CAAC,CAAC;IACnE,WAAW,EAAE,MAAM,CAAC,WAAW,CAAC,MAAM,CAAC,MAAM,CAAC;CAC/C,CAAC,CACH,CAAC;AAiBF;;;;GAIG;AACH,MAAM,CAAC,MAAM,MAAM,GAAG,CAOpB,UAAyC,EACzC,OAAsC,EACtC,EAAE;IACF,MAAM,IAAI,GAAG,OAAO,EAAE,IAAI,CAAC;IAE3B,4EAA4E;IAC5E,MAAM,KAAK,GAAG,UAAU,CAAC,IAAI,CAAC,UAAU,CAAC,EAAE,EAAE;QAC3C,MAAM,EAAE,UAAU,CAAC,UAAU,CAAC,MAAM,IAAI,MAAM,CAAC,SAAS;QACxD,GAAG,CAAC,IAAI,KAAK,SAAS,IAAI,EAAE,IAAI,EAAE,CAAC;QACnC,QAAQ,EAAE,CAAC,OAAO,EAAE,MAAM,CAAU;QACpC,OAAO,EAAE,OAAO;QAChB,QAAQ,EAAE,IAAI;KACf,CAAC,CAAC;IAEH,OAAO,CAAQ,KAA+B,EAA6C,EAAE,CAC3F,KAAK,CAAC,MAAM,CACV,UAAU,EACV,MAAM,CAAC,GAAG,CAAC,QAAQ,CAAC;QAClB,MAAM,KAAK,GAAG,KAAK,CAAC,CAAC,UAAU,CAAC;QAChC,MAAM,QAAQ,GAAG,KAAK,CAAC,CAAC,KAAK,CAAC,QAAQ,CAAC;QAEvC,OAAO,UAAU,CAAC,GAAG,CAAI,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,QAAQ,EAAE,EAAE,EAAE;YAC3D,IAAI,GAAG,KAAK,OAAO;gBAAE,OAAO,MAAM,CAAC,IAAI,CAAC;YAExC,MAAM,MAAM,GAAG,SAAS,CAAC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,SAAS,CAAC;YAE3E,OAAO,KAAK;iBACT,GAAG,CAAC;gBACH,MAAM,EAAE,IAAI,CAAC,CAAC,CAAC;gBACf,OAAO,EAAE,QAAQ,CAAC,IAAI;gBACtB,GAAG,CAAC,MAAM,EAAE,MAAM,KAAK,SAAS,IAAI,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,CAAC;gBAC9D,OAAO,EAAE,MAAM,IAAI;oBACjB,QAAQ,EAAE,MAAM,CAAC,EAAE;oBACnB,GAAG,CAAC,MAAM,CAAC,WAAW,KAAK,SAAS,IAAI,EAAE,WAAW,EAAE,MAAM,CAAC,WAAW,EAAE,CAAC;iBAC7E;aACF,CAAC;iBACD,IAAI,CAAC,MAAM,CAAC,cAAc,CAAC,KAAK,CAAC,QAAQ,EAAE,QAAQ,CAAC,CAAC,CAAC;QAC3D,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC;IACZ,CAAC,CAAC,CACH,CAAC,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC;AACjC,CAAC,CAAC"}
@@ -0,0 +1,18 @@
1
+ import type * as Condition from "../Condition.ts";
2
+ import { type Column, type SQL } from "drizzle-orm";
3
+ /**
4
+ * Column kinds whose SQL equality is JavaScript's, so a query selects exactly
5
+ * the rows Condition.matches accepts. Excluded: numeric (decimal text), char
6
+ * (space padded), string-mode bigints, citext and other custom types.
7
+ */
8
+ type Comparable = "string" | "string enum" | "string uuid" | "number int16" | "number int32" | "number int53" | "bigint int64" | "boolean" | "number float" | "number double";
9
+ /** A comparable column for every field a condition over `T` can compare; a Drizzle table qualifies. */
10
+ export type Columns<T> = {
11
+ readonly [K in Condition.Field<T>]: Column & {
12
+ readonly dataType: Comparable;
13
+ };
14
+ };
15
+ /** Translates a condition, such as a PermissionFilter's, into a PostgreSQL `where` clause. */
16
+ export declare const where: <T>(columns: Columns<T>, condition: Condition.Condition<T>) => SQL;
17
+ export {};
18
+ //# sourceMappingURL=postgres.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"postgres.d.ts","sourceRoot":"","sources":["../../src/drizzle/postgres.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,SAAS,MAAM,iBAAiB,CAAC;AAElD,OAAO,EAAO,KAAK,MAAM,EAAkB,KAAK,GAAG,EAAO,MAAM,aAAa,CAAC;AAG9E;;;;GAIG;AACH,KAAK,UAAU,GACX,QAAQ,GACR,aAAa,GACb,aAAa,GACb,cAAc,GACd,cAAc,GACd,cAAc,GACd,cAAc,GACd,SAAS,GACT,cAAc,GACd,eAAe,CAAC;AAsBpB,uGAAuG;AACvG,MAAM,MAAM,OAAO,CAAC,CAAC,IAAI;IACvB,QAAQ,EAAE,CAAC,IAAI,SAAS,CAAC,KAAK,CAAC,CAAC,CAAC,GAAG,MAAM,GAAG;QAAE,QAAQ,CAAC,QAAQ,EAAE,UAAU,CAAA;KAAE;CAC/E,CAAC;AAEF,8FAA8F;AAC9F,eAAO,MAAM,KAAK,GAAI,CAAC,WAAW,OAAO,CAAC,CAAC,CAAC,aAAa,SAAS,CAAC,SAAS,CAAC,CAAC,CAAC,KAAG,GA2DjF,CAAC"}
@@ -0,0 +1,65 @@
1
+ import { and, eq, isNull, or, sql } from "drizzle-orm";
2
+ import { Match, Predicate } from "effect";
3
+ // char shares text's dataType and numeric's bigint mode shares bigint's, so
4
+ // the runtime check names column classes too.
5
+ const comparable = new Set([
6
+ "PgText",
7
+ "PgVarchar",
8
+ "PgEnumColumn",
9
+ "PgEnumObjectColumn",
10
+ "PgUUID",
11
+ "PgSmallInt",
12
+ "PgInteger",
13
+ "PgBigInt53",
14
+ "PgBigInt64",
15
+ "PgBoolean",
16
+ "PgReal",
17
+ "PgDoublePrecision",
18
+ ]);
19
+ // PostgreSQL reads uuids in any case and writes them lowercase.
20
+ const canonicalUuid = /^[\da-f]{8}-[\da-f]{4}-[\da-f]{4}-[\da-f]{4}-[\da-f]{12}$/;
21
+ /** Translates a condition, such as a PermissionFilter's, into a PostgreSQL `where` clause. */
22
+ export const where = (columns, condition) => {
23
+ const byField = new Map(Object.entries(columns));
24
+ const column = (field) => {
25
+ const found = byField.get(field);
26
+ if (found === undefined)
27
+ throw new Error(`No column for condition field ${field}`);
28
+ if (!comparable.has(found.columnType))
29
+ throw new Error(`Column ${field} (${found.getSQLType()}) compares differently in PostgreSQL than in JavaScript`);
30
+ return found;
31
+ };
32
+ const value = (target, compared) => {
33
+ if (target.columnType === "PgUUID" &&
34
+ !(Predicate.isString(compared) && canonicalUuid.test(compared)))
35
+ throw new Error(`Uuid ${String(compared)} is not lowercase and canonical`);
36
+ return compared;
37
+ };
38
+ const translate = Match.type().pipe(Match.tagsExhaustive({
39
+ All: () => sql `true`,
40
+ None: () => sql `false`,
41
+ Eq: ({ field, value: compared }) => {
42
+ const target = column(field);
43
+ return compared === null ? isNull(target) : eq(target, value(target, compared));
44
+ },
45
+ OneOf: ({ field, values }) => {
46
+ const target = column(field);
47
+ const known = values.filter((each) => each !== null).map((each) => value(target, each));
48
+ // One array parameter however long the list: @effect/sql-pg sends at
49
+ // most 32767 parameters per statement.
50
+ const listed = known.length === 0
51
+ ? sql `false`
52
+ : sql `${target} = any(${sql.param(known)}::${sql.raw(target.getSQLType())}[])`;
53
+ // IN and ANY never match NULL, while Condition.matches finds null in the list.
54
+ return values.includes(null) ? (or(listed, isNull(target)) ?? listed) : listed;
55
+ },
56
+ And: ({ conditions }) => and(sql `true`, ...conditions.map(translate)) ?? sql `true`,
57
+ Or: ({ conditions }) => or(sql `false`, ...conditions.map(translate)) ?? sql `false`,
58
+ // Comparisons with NULL columns are NULL, which WHERE, AND and OR treat as
59
+ // false like Condition.matches does; NOT would keep it NULL, so negate with
60
+ // IS NOT TRUE instead.
61
+ Not: ({ condition: inner }) => sql `(${translate(inner)}) is not true`,
62
+ }));
63
+ return translate(condition);
64
+ };
65
+ //# sourceMappingURL=postgres.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"postgres.js","sourceRoot":"","sources":["../../src/drizzle/postgres.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,5 @@
1
+ export * as Condition from "./Condition.ts";
2
+ export * as Decision from "./Decision.ts";
3
+ export * as Permission from "./Permission.ts";
4
+ export * as PermissionFilter from "./PermissionFilter.ts";
5
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,SAAS,MAAM,gBAAgB,CAAC;AAE5C,OAAO,KAAK,QAAQ,MAAM,eAAe,CAAC;AAE1C,OAAO,KAAK,UAAU,MAAM,iBAAiB,CAAC;AAE9C,OAAO,KAAK,gBAAgB,MAAM,uBAAuB,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,5 @@
1
+ export * as Condition from "./Condition.js";
2
+ export * as Decision from "./Decision.js";
3
+ export * as Permission from "./Permission.js";
4
+ export * as PermissionFilter from "./PermissionFilter.js";
5
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,SAAS,MAAM,gBAAgB,CAAC;AAE5C,OAAO,KAAK,QAAQ,MAAM,eAAe,CAAC;AAE1C,OAAO,KAAK,UAAU,MAAM,iBAAiB,CAAC;AAE9C,OAAO,KAAK,gBAAgB,MAAM,uBAAuB,CAAC"}
@@ -0,0 +1,17 @@
1
+ import type * as Decision from "../Decision.ts";
2
+ import { Effect } from "effect";
3
+ import * as Condition from "../Condition.ts";
4
+ export interface Checkable<T, E, R> {
5
+ readonly id: string;
6
+ readonly check: (target: T) => Effect.Effect<Decision.Decision, E, R>;
7
+ }
8
+ export interface Filter<T, E, R> {
9
+ readonly condition: Effect.Effect<Condition.Condition<T>, E, R>;
10
+ }
11
+ /**
12
+ * Dies unless a PermissionFilter's condition matches exactly the targets its
13
+ * permission allows, so lists never show what a direct read would refuse, or
14
+ * hide what it would allow.
15
+ */
16
+ export declare const assertFilterAgrees: <T extends object, E, RCheck, RFilter>(permission: Checkable<T, E, RCheck>, filter: Filter<T, E, RFilter>, targets: Iterable<T>) => Effect.Effect<void, E, RCheck | RFilter>;
17
+ //# sourceMappingURL=PermissionTesting.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"PermissionTesting.d.ts","sourceRoot":"","sources":["../../src/testing/PermissionTesting.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,QAAQ,MAAM,gBAAgB,CAAC;AAEhD,OAAO,EAAE,MAAM,EAAa,MAAM,QAAQ,CAAC;AAE3C,OAAO,KAAK,SAAS,MAAM,iBAAiB,CAAC;AAE7C,MAAM,WAAW,SAAS,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC;IAChC,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,KAAK,EAAE,CAAC,MAAM,EAAE,CAAC,KAAK,MAAM,CAAC,MAAM,CAAC,QAAQ,CAAC,QAAQ,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC;CACvE;AAED,MAAM,WAAW,MAAM,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC;IAC7B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC,MAAM,CAAC,SAAS,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC;CACjE;AAED;;;;GAIG;AACH,eAAO,MAAM,kBAAkB,GAC7B,CAAC,SAAS,MAAM,EAChB,CAAC,EACD,MAAM,EACN,OAAO,uIAsBP,CAAC"}
@@ -0,0 +1,19 @@
1
+ import { Effect, Predicate } from "effect";
2
+ import * as Condition from "../Condition.js";
3
+ /**
4
+ * Dies unless a PermissionFilter's condition matches exactly the targets its
5
+ * permission allows, so lists never show what a direct read would refuse, or
6
+ * hide what it would allow.
7
+ */
8
+ export const assertFilterAgrees = Effect.fnUntraced(function* (permission, filter, targets) {
9
+ const condition = yield* filter.condition;
10
+ const decided = yield* Effect.forEach(targets, (target) => Effect.map(permission.check(target), (decision) => ({
11
+ target,
12
+ allowed: Predicate.isTagged(decision, "Allow"),
13
+ })));
14
+ const mismatch = decided.find(({ target, allowed }) => allowed !== Condition.matches(condition, target));
15
+ return yield* mismatch === undefined
16
+ ? Effect.void
17
+ : Effect.die(new Error(`The filter of permission ${permission.id} ${mismatch.allowed ? "excludes" : "matches"} a target the permission ${mismatch.allowed ? "allows" : "refuses"}: ${JSON.stringify(mismatch.target)}`));
18
+ });
19
+ //# sourceMappingURL=PermissionTesting.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"PermissionTesting.js","sourceRoot":"","sources":["../../src/testing/PermissionTesting.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,QAAQ,CAAC;AAE3C,OAAO,KAAK,SAAS,MAAM,iBAAiB,CAAC;AAW7C;;;;GAIG;AACH,MAAM,CAAC,MAAM,kBAAkB,GAAG,MAAM,CAAC,UAAU,CAAC,QAAQ,CAAC,EAK3D,UAAmC,EAAE,MAA6B,EAAE,OAAoB;IACxF,MAAM,SAAS,GAAG,KAAK,CAAC,CAAC,MAAM,CAAC,SAAS,CAAC;IAE1C,MAAM,OAAO,GAAG,KAAK,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,OAAO,EAAE,CAAC,MAAM,EAAE,EAAE,CACxD,MAAM,CAAC,GAAG,CAAC,UAAU,CAAC,KAAK,CAAC,MAAM,CAAC,EAAE,CAAC,QAAQ,EAAE,EAAE,CAAC,CAAC;QAClD,MAAM;QACN,OAAO,EAAE,SAAS,CAAC,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC;KAC/C,CAAC,CAAC,CACJ,CAAC;IAEF,MAAM,QAAQ,GAAG,OAAO,CAAC,IAAI,CAC3B,CAAC,EAAE,MAAM,EAAE,OAAO,EAAE,EAAE,EAAE,CAAC,OAAO,KAAK,SAAS,CAAC,OAAO,CAAC,SAAS,EAAE,MAAM,CAAC,CAC1E,CAAC;IAEF,OAAO,KAAK,CAAC,CAAC,QAAQ,KAAK,SAAS;QAClC,CAAC,CAAC,MAAM,CAAC,IAAI;QACb,CAAC,CAAC,MAAM,CAAC,GAAG,CACR,IAAI,KAAK,CACP,4BAA4B,UAAU,CAAC,EAAE,IAAI,QAAQ,CAAC,OAAO,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,SAAS,4BAA4B,QAAQ,CAAC,OAAO,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,SAAS,KAAK,IAAI,CAAC,SAAS,CAAC,QAAQ,CAAC,MAAM,CAAC,EAAE,CAChM,CACF,CAAC;AACR,CAAC,CAAC,CAAC"}
@@ -0,0 +1,2 @@
1
+ export * as PermissionTesting from "./PermissionTesting.ts";
2
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/testing/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,iBAAiB,MAAM,wBAAwB,CAAC"}
@@ -0,0 +1,2 @@
1
+ export * as PermissionTesting from "./PermissionTesting.js";
2
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/testing/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,iBAAiB,MAAM,wBAAwB,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,91 @@
1
1
  {
2
2
  "name": "@kutur-kit/access",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "0.1.0-alpha.1",
4
+ "description": "Effect permissions as services: typed checks, caller restrictions, list filters and audit.",
5
+ "license": "MIT",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/kutur-labs/kit.git",
9
+ "directory": "packages/access"
10
+ },
11
+ "files": [
12
+ "src",
13
+ "dist"
14
+ ],
15
+ "type": "module",
16
+ "sideEffects": false,
17
+ "exports": {
18
+ ".": {
19
+ "types": "./dist/index.d.ts",
20
+ "default": "./dist/index.js"
21
+ },
22
+ "./Condition": {
23
+ "types": "./dist/Condition.d.ts",
24
+ "default": "./dist/Condition.js"
25
+ },
26
+ "./Decision": {
27
+ "types": "./dist/Decision.d.ts",
28
+ "default": "./dist/Decision.js"
29
+ },
30
+ "./Permission": {
31
+ "types": "./dist/Permission.d.ts",
32
+ "default": "./dist/Permission.js"
33
+ },
34
+ "./PermissionFilter": {
35
+ "types": "./dist/PermissionFilter.d.ts",
36
+ "default": "./dist/PermissionFilter.js"
37
+ },
38
+ "./audit": {
39
+ "types": "./dist/audit.d.ts",
40
+ "default": "./dist/audit.js"
41
+ },
42
+ "./drizzle/postgres": {
43
+ "types": "./dist/drizzle/postgres.d.ts",
44
+ "default": "./dist/drizzle/postgres.js"
45
+ },
46
+ "./testing": {
47
+ "types": "./dist/testing/index.d.ts",
48
+ "default": "./dist/testing/index.js"
49
+ }
50
+ },
51
+ "publishConfig": {
52
+ "access": "public"
53
+ },
54
+ "devDependencies": {
55
+ "@effect/sql-pg": "4.0.5",
56
+ "@effect/tsgo": "0.53.3",
57
+ "@effect/vitest": "4.0.5",
58
+ "@kutur-kit/audit": "0.1.0-alpha.1",
59
+ "@kutur-kit/testcontainers": "0.0.0",
60
+ "@kutur-kit/tsconfig": "0.1.0-alpha.0",
61
+ "@stryker-mutator/core": "10.0.0",
62
+ "@stryker-mutator/vitest-runner": "10.0.0",
63
+ "@types/node": "24.19.2",
64
+ "drizzle-kit": "1.0.0-rc.4",
65
+ "drizzle-orm": "1.0.0-rc.4",
66
+ "effect": "4.0.5",
67
+ "publint": "0.3.25",
68
+ "typescript": "7.0.2",
69
+ "vitest": "5.0.5"
70
+ },
71
+ "peerDependencies": {
72
+ "@kutur-kit/audit": "^0.1.0-alpha.1",
73
+ "drizzle-orm": "1.0.0-rc.4",
74
+ "effect": "^4.0.5"
75
+ },
76
+ "peerDependenciesMeta": {
77
+ "@kutur-kit/audit": {
78
+ "optional": true
79
+ },
80
+ "drizzle-orm": {
81
+ "optional": true
82
+ }
83
+ },
84
+ "scripts": {
85
+ "build": "node --input-type=module -e 'import { rmSync } from \"node:fs\"; rmSync(\"dist\", { recursive: true, force: true });' && tsc -p tsconfig.build.json",
86
+ "lint:package": "publint --pack pnpm --strict",
87
+ "test": "vitest run",
88
+ "test:mutation": "stryker run",
89
+ "typecheck": "tsc -p tsconfig.json --noEmit"
90
+ }
6
91
  }