bazis 0.140.0 → 0.140.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.
@@ -2,8 +2,8 @@ import { Module, singleton } from "../di";
2
2
  import { AccessCatalog } from "./catalog/AccessCatalog";
3
3
  import { IAccessCatalog } from "./catalog/IAccessCatalog.service";
4
4
  import { definePermissions } from "./catalog/permissions";
5
- import { PermissionSourceLoader } from "./catalog/PermissionSourceLoader";
6
- import { PermissionSources } from "./catalog/PermissionSources";
5
+ import { PermissionSourceLoader } from "./catalog/PermissionSourceLoader.background";
6
+ import { PermissionSourcesService } from "./catalog/PermissionSources.service";
7
7
  import { Access } from "./check/Access";
8
8
  import { accessConfig } from "./check/accessConfig";
9
9
  import { IAccess } from "./check/IAccess.service";
@@ -29,7 +29,7 @@ export const accessPermissions = definePermissions({
29
29
  config: [accessConfig],
30
30
  permissions: accessPermissions,
31
31
  providers: [
32
- singleton(PermissionSources),
32
+ singleton(PermissionSourcesService),
33
33
  singleton(IAccessCatalog, AccessCatalog),
34
34
  singleton(AccessPolicy, RolesAccessPolicy),
35
35
  singleton(IAccess, Access),
@@ -9,7 +9,7 @@ import type { RolePermission } from "../roles/roles";
9
9
  import { IAccessCatalog } from "./IAccessCatalog.service";
10
10
  import { declarationsOf } from "./moduleFields";
11
11
  import type { PermissionSpec } from "./permissions";
12
- import type { PermissionSources, PermissionSourceState } from "./PermissionSources";
12
+ import type { PermissionSourcesService, PermissionSourceState } from "./PermissionSources.service";
13
13
 
14
14
  /** A permission with the module that declares it: in `@Module({ permissions })` or through a source. */
15
15
  export interface OwnedPermission {
@@ -40,7 +40,10 @@ export class AccessCatalog extends IAccessCatalog {
40
40
  private readonly byKind: ReadonlyMap<string, OwnedReferenceKind>;
41
41
  private readonly byName: ReadonlyMap<string, DeclaredRole>;
42
42
 
43
- constructor(services: ServiceProvider, private readonly sourced: PermissionSources, private readonly config: ConfigView<AccessConfig>) {
43
+ /** The declared permissions and the ones of the sources as one sorted list, good for one revision of the sources. */
44
+ private merged: { readonly revision: number; readonly list: readonly OwnedPermission[] } | undefined;
45
+
46
+ constructor(services: ServiceProvider, private readonly sourced: PermissionSourcesService, private readonly config: ConfigView<AccessConfig>) {
44
47
  super();
45
48
  const { permissions, kinds, roles } = declarationsOf(services);
46
49
  this.byKey = new Map([...permissions].sort((a, b) => a.key.localeCompare(b.key)).map((item) => [item.key, item]));
@@ -49,8 +52,11 @@ export class AccessCatalog extends IAccessCatalog {
49
52
  }
50
53
 
51
54
  permissions(): readonly OwnedPermission[] {
52
- const sourced = this.sourced.permissions();
53
- return sourced.length === 0 ? [...this.byKey.values()] : [...this.byKey.values(), ...sourced].sort((a, b) => a.key.localeCompare(b.key));
55
+ const revision = this.sourced.revision;
56
+ if (this.merged?.revision !== revision) {
57
+ this.merged = { revision, list: [...this.byKey.values(), ...this.sourced.permissions()].sort((a, b) => a.key.localeCompare(b.key)) };
58
+ }
59
+ return [...this.merged.list];
54
60
  }
55
61
 
56
62
  permission(key: string): OwnedPermission | undefined {
@@ -1,6 +1,6 @@
1
1
  import type { Grant } from "../contracts/types";
2
2
  import type { DeclaredRole, OwnedPermission, OwnedReferenceKind } from "./AccessCatalog";
3
- import type { PermissionSourceState } from "./PermissionSources";
3
+ import type { PermissionSourceState } from "./PermissionSources.service";
4
4
 
5
5
  /**
6
6
  * What the application's modules declared about access: permissions, reference
@@ -1,14 +1,19 @@
1
1
  import type { HostedService, HostedServiceHealth } from "../../di";
2
- import type { PermissionSources } from "./PermissionSources";
2
+ import type { PermissionSourcesService } from "./PermissionSources.service";
3
3
 
4
4
  /** After the infrastructure and the schema (phases below), before the HTTP host (phase 10). */
5
5
  const PHASE = 5;
6
+ /** How often a failed source is tried again when the versions are not polled. */
7
+ const RETRY_SECONDS = 5;
8
+ /** A failure or two is a database that blinked; `/health` reports a source that keeps failing. */
9
+ const UNHEALTHY_AFTER = 3;
6
10
 
7
11
  /**
8
12
  * Reads the permission sources before the application takes requests and
9
13
  * then asks their versions every `access.sourcePollSeconds` (ACCESS-001 §3.1).
10
14
  * A source that fails does not stop the start: its permissions do not exist
11
- * until a later reading succeeds, and `/health` says so.
15
+ * until a later reading succeeds, and `/health` says so. Without a poll
16
+ * (`sourcePollSeconds: 0`) only the sources that failed are tried again.
12
17
  */
13
18
  export class PermissionSourceLoader implements HostedService {
14
19
  readonly phase = PHASE;
@@ -16,16 +21,16 @@ export class PermissionSourceLoader implements HostedService {
16
21
  private timer: ReturnType<typeof setInterval> | undefined;
17
22
  private polling: Promise<void> | undefined;
18
23
 
19
- constructor(private readonly sources: PermissionSources) {}
24
+ constructor(private readonly sources: PermissionSourcesService) {}
20
25
 
21
26
  async start(): Promise<void> {
27
+ if (this.sources.count === 0) return;
22
28
  await this.sources.load();
23
- const seconds = this.sources.pollSeconds;
24
- if (seconds <= 0) return;
29
+ const poll = this.sources.pollSeconds;
25
30
  this.timer = setInterval(() => {
26
31
  // One question at a time: a slow source is not asked again before it answers.
27
- this.polling ??= this.sources.poll().finally(() => { this.polling = undefined; });
28
- }, seconds * 1000);
32
+ this.polling ??= (poll > 0 ? this.sources.poll() : this.sources.retry()).catch(() => undefined).finally(() => { this.polling = undefined; });
33
+ }, (poll > 0 ? poll : RETRY_SECONDS) * 1000);
29
34
  this.timer.unref?.();
30
35
  }
31
36
 
@@ -35,8 +40,9 @@ export class PermissionSourceLoader implements HostedService {
35
40
  await this.polling;
36
41
  }
37
42
 
43
+ /** Unhealthy: a source without permissions at all, or one whose readings keep failing. */
38
44
  checkHealth(): HostedServiceHealth {
39
- const failed = this.sources.state().filter((source) => !source.loaded || source.error !== undefined);
45
+ const failed = this.sources.state().filter((source) => !source.loaded || source.failures >= UNHEALTHY_AFTER);
40
46
  if (failed.length === 0) return { healthy: true };
41
47
  return { healthy: false, details: failed.map((source) => `permission source "${source.prefix}": ${source.loaded ? "" : "not loaded: "}${source.error ?? "no reading yet"}`).join("; ") };
42
48
  }
@@ -23,6 +23,8 @@ export interface PermissionSourceState {
23
23
  readonly version: string | undefined;
24
24
  /** Why the last reading failed; the permissions read before it stay. */
25
25
  readonly error: string | undefined;
26
+ /** Readings and version questions that failed in a row; `0` after a success. */
27
+ readonly failures: number;
26
28
  readonly permissions: number;
27
29
  }
28
30
 
@@ -31,10 +33,13 @@ interface Source extends OwnedPermissionSource {
31
33
  loaded: boolean;
32
34
  version: string | undefined;
33
35
  error: string | undefined;
34
- /** When the version was last compared, for the limit of {@link PermissionSources.verify}. */
36
+ failures: number;
37
+ /** When the version was last compared, for the limit of {@link PermissionSourcesService.verify}. */
35
38
  verifiedAt: number;
36
39
  /** Readings of one source go one after another. */
37
40
  queue: Promise<unknown>;
41
+ /** A call that ran out of time and has not answered yet: no new call starts beside it. */
42
+ unanswered: Promise<void> | undefined;
38
43
  }
39
44
 
40
45
  /** How often a missing key or attribute may ask a source for its version. */
@@ -45,18 +50,21 @@ const VERIFY_EVERY_MS = 1000;
45
50
  * the application takes requests, read again when a source's version changes.
46
51
  * A specification passes the checks of a declared one; a bad one is logged and
47
52
  * the permission does not exist — closed, never open. A reading that fails
48
- * leaves the permissions read before it.
53
+ * leaves the permissions read before it. A source that does not answer in
54
+ * `access.sourceTimeoutSeconds` has failed: it holds neither the start nor a
55
+ * request.
49
56
  */
50
- export class PermissionSources {
57
+ export class PermissionSourcesService {
51
58
  private readonly sources: readonly Source[];
52
59
  private readonly kinds: ReadonlySet<string>;
60
+ private changes = 0;
53
61
 
54
62
  constructor(private readonly services: ServiceProvider, private readonly config: ConfigView<AccessConfig>) {
55
63
  const { sources, kinds } = declarationsOf(services);
56
64
  this.kinds = new Set(kinds.map((kind) => kind.kind));
57
65
  this.sources = [...sources]
58
66
  .sort((left, right) => left.prefix.localeCompare(right.prefix))
59
- .map((source) => ({ ...source, permissions: new Map(), loaded: false, version: undefined, error: undefined, verifiedAt: 0, queue: Promise.resolve() }));
67
+ .map((source) => ({ ...source, permissions: new Map(), loaded: false, version: undefined, error: undefined, failures: 0, verifiedAt: 0, queue: Promise.resolve(), unanswered: undefined }));
60
68
  }
61
69
 
62
70
  /** The prefix of the source that owns the key, or `undefined` — the key is not of a source. */
@@ -74,7 +82,16 @@ export class PermissionSources {
74
82
  }
75
83
 
76
84
  state(): readonly PermissionSourceState[] {
77
- return this.sources.map(({ prefix, module, loaded, version, error, permissions }) => ({ prefix, module, loaded, version, error, permissions: permissions.size }));
85
+ return this.sources.map(({ prefix, module, loaded, version, error, failures, permissions }) => ({ prefix, module, loaded, version, error, failures, permissions: permissions.size }));
86
+ }
87
+
88
+ /** Grows with every reading that changed the permissions: a list built from them is good until it does. */
89
+ get revision(): number {
90
+ return this.changes;
91
+ }
92
+
93
+ get count(): number {
94
+ return this.sources.length;
78
95
  }
79
96
 
80
97
  /** The seconds between two questions about the versions; `0` — never ask. */
@@ -92,6 +109,13 @@ export class PermissionSources {
92
109
  await Promise.all(this.sources.map((source) => this.enqueue(source, () => this.compare(source))));
93
110
  }
94
111
 
112
+ /** Tries again the sources whose last reading failed: without a poll nothing else would. */
113
+ async retry(): Promise<void> {
114
+ await Promise.all(this.sources
115
+ .filter((source) => !source.loaded || source.error !== undefined)
116
+ .map((source) => this.enqueue(source, () => this.compare(source))));
117
+ }
118
+
95
119
  /**
96
120
  * The source of the prefix changed. With `keys` only these permissions are
97
121
  * read now; the version stays the old one, so the next comparison reads the
@@ -113,7 +137,9 @@ export class PermissionSources {
113
137
  const source = this.sources.find((item) => underPrefix(key, item.prefix));
114
138
  if (source === undefined) return false;
115
139
  return await this.enqueue(source, async () => {
116
- if (!force && Date.now() - source.verifiedAt < VERIFY_EVERY_MS) return false;
140
+ // A clock set back gives a negative time: compare then, do not wait for the clock to catch up.
141
+ const since = Date.now() - source.verifiedAt;
142
+ if (!force && since >= 0 && since < VERIFY_EVERY_MS) return false;
117
143
  return await this.compare(source);
118
144
  });
119
145
  }
@@ -129,9 +155,10 @@ export class PermissionSources {
129
155
  source.verifiedAt = Date.now();
130
156
  if (source.loaded) {
131
157
  try {
132
- const version = await this.use(source, async (instance) => String(await instance.version()));
158
+ const version = await this.use(source, async (instance) => versionOf(await instance.version()));
133
159
  if (version === source.version) {
134
160
  source.error = undefined;
161
+ source.failures = 0;
135
162
  return false;
136
163
  }
137
164
  } catch (error) {
@@ -146,7 +173,7 @@ export class PermissionSources {
146
173
  source.verifiedAt = Date.now();
147
174
  try {
148
175
  // The version first: data changed during the reading gives a newer version to the next comparison.
149
- const { version, declarations } = await this.use(source, async (instance) => ({ version: String(await instance.version()), declarations: await instance.load() }));
176
+ const { version, declarations } = await this.use(source, async (instance) => ({ version: versionOf(await instance.version()), declarations: await instance.load() }));
150
177
  if (typeof declarations !== "object" || declarations === null || Array.isArray(declarations)) throw new TypeError("load() must return an object: key → permission");
151
178
  const permissions = new Map<string, OwnedPermission>();
152
179
  for (const key of Object.keys(declarations).sort()) {
@@ -157,6 +184,8 @@ export class PermissionSources {
157
184
  source.version = version;
158
185
  source.loaded = true;
159
186
  source.error = undefined;
187
+ source.failures = 0;
188
+ this.changes += 1;
160
189
  return true;
161
190
  } catch (error) {
162
191
  this.failed(source, error);
@@ -176,6 +205,9 @@ export class PermissionSources {
176
205
  else permissions.set(key, permission);
177
206
  }
178
207
  source.permissions = new Map([...permissions].sort(([left], [right]) => left.localeCompare(right)));
208
+ source.error = undefined;
209
+ source.failures = 0;
210
+ this.changes += 1;
179
211
  return true;
180
212
  } catch (error) {
181
213
  this.failed(source, error);
@@ -185,33 +217,61 @@ export class PermissionSources {
185
217
 
186
218
  /** The permission, or `undefined` with the problems logged: it does not exist then. */
187
219
  private accept(source: Source, key: string, spec: PermissionSpec): OwnedPermission | undefined {
188
- const problems = underPrefix(key, source.prefix) ? permissionProblems(key, spec) : [`permission "${key}": the key must start with "${source.prefix}."`];
189
- if (problems.length === 0) {
190
- for (const [name, attribute] of Object.entries(spec.on ?? {})) {
191
- if (attribute.ref !== undefined && !this.kinds.has(attribute.ref)) problems.push(`permission "${key}": attribute "${name}" refers to kind "${attribute.ref}", which no module declares in references`);
192
- }
193
- for (const lookup of lookupsOf(spec)) {
194
- if (!this.services.has(lookup)) problems.push(`permission "${key}": ${lookup.name} is not registered: add it to providers of ${source.module}`);
220
+ let problems: string[];
221
+ let accepted: OwnedPermission | undefined;
222
+ try {
223
+ problems = underPrefix(key, source.prefix) ? permissionProblems(key, spec) : [`permission "${key}": the key must start with "${source.prefix}."`];
224
+ if (problems.length === 0) {
225
+ for (const [name, attribute] of Object.entries(spec.on ?? {})) {
226
+ if (attribute.ref !== undefined && !this.kinds.has(attribute.ref)) problems.push(`permission "${key}": attribute "${name}" refers to kind "${attribute.ref}", which no module declares in references`);
227
+ }
228
+ for (const lookup of lookupsOf(spec)) {
229
+ if (!this.services.has(lookup)) problems.push(`permission "${key}": ${lookup.name} is not registered: add it to providers of ${source.module}`);
230
+ }
195
231
  }
232
+ if (problems.length === 0) accepted = { key, spec: frozenSpec(spec), module: source.module, source: source.prefix };
233
+ } catch (error) {
234
+ // Data of any shape may come from a source: what cannot even be checked is one bad permission, not a failed reading.
235
+ problems = [`permission "${key}": cannot be checked: ${error instanceof Error ? error.message : String(error)}`];
196
236
  }
197
- if (problems.length > 0) {
198
- this.services.tryResolve(LOGGER)?.error("access permission of a source rejected", { source: source.prefix, module: source.module, permission: key, problems });
199
- return undefined;
200
- }
201
- return { key, spec: frozenSpec(spec), module: source.module, source: source.prefix };
237
+ if (accepted === undefined) this.services.tryResolve(LOGGER)?.error("access permission of a source rejected", { source: source.prefix, module: source.module, permission: key, problems });
238
+ return accepted;
202
239
  }
203
240
 
241
+ /**
242
+ * One call of the source in a scope of its own, with a deadline. A call that
243
+ * ran out of time cannot be stopped: its scope is closed when it answers, and
244
+ * until then no new call starts, so a database that hangs is asked once.
245
+ */
204
246
  private async use<T>(source: Source, work: (instance: PermissionSource) => Promise<T>): Promise<T> {
247
+ if (source.unanswered !== undefined) throw new Error("the previous call of the source has not answered yet");
248
+ const seconds = this.config.get("sourceTimeoutSeconds");
205
249
  const scope = this.services.createScope();
250
+ const working = (async () => await work(scope.resolve(source.type)))();
251
+ const answered = working.then(() => undefined, () => undefined).then(async () => {
252
+ source.unanswered = undefined;
253
+ await scope.dispose().catch(() => undefined);
254
+ });
255
+ let late = false;
256
+ let timer: ReturnType<typeof setTimeout> | undefined;
257
+ const deadline = new Promise<never>((_, reject) => {
258
+ timer = setTimeout(() => {
259
+ late = true;
260
+ source.unanswered = answered;
261
+ reject(new Error(`the source did not answer in ${seconds} s (access.sourceTimeoutSeconds)`));
262
+ }, seconds * 1000);
263
+ });
206
264
  try {
207
- return await work(scope.resolve(source.type));
265
+ return await Promise.race([working, deadline]);
208
266
  } finally {
209
- await scope.dispose();
267
+ clearTimeout(timer);
268
+ if (!late) await answered;
210
269
  }
211
270
  }
212
271
 
213
272
  private failed(source: Source, error: unknown): void {
214
273
  source.error = error instanceof Error ? error.message : String(error);
274
+ source.failures += 1;
215
275
  this.services.tryResolve(LOGGER)?.error("access permission source failed", {
216
276
  source: source.prefix,
217
277
  module: source.module,
@@ -220,3 +280,13 @@ export class PermissionSources {
220
280
  });
221
281
  }
222
282
  }
283
+
284
+ /**
285
+ * A version is what the catalog compares to learn of a change: `undefined`,
286
+ * `null` or an empty string would look like a version that never changes.
287
+ */
288
+ function versionOf(value: unknown): string {
289
+ if (typeof value === "string" && value !== "") return value;
290
+ if ((typeof value === "number" && Number.isFinite(value)) || typeof value === "bigint") return String(value);
291
+ throw new TypeError(`version() must return a non-empty string or a number, got ${value === null ? "null" : typeof value === "string" ? "an empty string" : typeof value}`);
292
+ }
@@ -17,7 +17,7 @@ import { isDeclaredRoles, rolePermission, type RoleDeclarations, type RolePermis
17
17
  import { IAccess } from "../check/IAccess.service";
18
18
  import type { DeclaredRole, OwnedPermission, OwnedReferenceKind } from "./AccessCatalog";
19
19
  import { isDeclaredPermissions, isReferenceKind, isSourcePrefix, lookupsOf, permissionProblems, underPrefix, type PermissionDeclarations, type PermissionSpec } from "./permissions";
20
- import type { OwnedPermissionSource } from "./PermissionSources";
20
+ import type { OwnedPermissionSource } from "./PermissionSources.service";
21
21
 
22
22
  interface PermissionPayload { readonly key: string; readonly spec: PermissionSpec }
23
23
  interface ReferencePayload { readonly kind: string; readonly type: Class<References> }
@@ -97,7 +97,9 @@ export function permissionProblems(key: string, spec: PermissionSpec): string[]
97
97
  if (!isText(spec.title, 200)) problems.push(`${at}: title must be a non-empty text up to 200 characters`);
98
98
  if (spec.description !== undefined && !isText(spec.description, 2000)) problems.push(`${at}: description must be a non-empty text up to 2000 characters`);
99
99
  const scope = spec.scope;
100
- if (scope !== undefined && scope !== "none" && scope !== "request") {
100
+ if (typeof scope !== "object" || scope === null) {
101
+ if (scope !== undefined && scope !== "none" && scope !== "request") problems.push(`${at}: scope must be "none", "request", { column } or { lookup }`);
102
+ } else {
101
103
  const column = (scope as { column?: unknown }).column;
102
104
  const lookup = (scope as { lookup?: unknown }).lookup;
103
105
  if ((column === undefined) === (lookup === undefined)) problems.push(`${at}: scope must be "none", "request", { column } or { lookup }`);
@@ -126,7 +128,7 @@ export function permissionProblems(key: string, spec: PermissionSpec): string[]
126
128
  /** The lookup classes a permission needs registered. */
127
129
  export function lookupsOf(spec: PermissionSpec): Class<AttributeLookup>[] {
128
130
  const lookups: Class<AttributeLookup>[] = [];
129
- if (typeof spec.scope === "object" && "lookup" in spec.scope) lookups.push(spec.scope.lookup);
131
+ if (typeof spec.scope === "object" && spec.scope !== null && "lookup" in spec.scope) lookups.push(spec.scope.lookup);
130
132
  for (const attribute of Object.values(spec.on ?? {})) if (attribute.lookup !== undefined) lookups.push(attribute.lookup);
131
133
  return lookups;
132
134
  }
@@ -149,7 +151,7 @@ export function underPrefix(key: string, prefix: string): boolean {
149
151
 
150
152
  /** A frozen copy of a specification a source gave: the catalog keeps it, the source may change its own. */
151
153
  export function frozenSpec(spec: PermissionSpec): PermissionSpec {
152
- const scope = typeof spec.scope === "object" ? { ...spec.scope } : spec.scope;
154
+ const scope = typeof spec.scope === "object" && spec.scope !== null ? { ...spec.scope } : spec.scope;
153
155
  const on = spec.on === undefined ? undefined : Object.fromEntries(Object.entries(spec.on).map(([name, attribute]) => [name, { ...attribute }]));
154
156
  const title = typeof spec.title === "object" ? { ...spec.title } : spec.title;
155
157
  const description = typeof spec.description === "object" ? { ...spec.description } : spec.description;
@@ -15,12 +15,15 @@ export interface AccessConfig {
15
15
  * `IAccessCatalog.changed` on every instance itself.
16
16
  */
17
17
  sourcePollSeconds: number;
18
+ /** Seconds a permission source has for one answer; after them the reading has failed. */
19
+ sourceTimeoutSeconds: number;
18
20
  }
19
21
 
20
22
  export const accessConfig = defineConfig<AccessConfig>("access", {
21
- default: { kindClaim: "", roleClaim: "role", maxInList: 1000, sourcePollSeconds: 5 },
23
+ default: { kindClaim: "", roleClaim: "role", maxInList: 1000, sourcePollSeconds: 5, sourceTimeoutSeconds: 10 },
22
24
  validate: {
23
25
  maxInList: (value) => Number.isSafeInteger(value) && value >= 1 && value <= 100_000 ? undefined : "must be an integer 1–100000",
24
26
  sourcePollSeconds: (value) => Number.isFinite(value) && value >= 0 && value <= 3600 ? undefined : "must be a number 0–3600",
27
+ sourceTimeoutSeconds: (value) => Number.isFinite(value) && value > 0 && value <= 600 ? undefined : "must be a number above 0, up to 600",
25
28
  },
26
29
  });
@@ -20,9 +20,10 @@ export abstract class PermissionSource {
20
20
  * The version of the source's data: a counter or a timestamp that changes in
21
21
  * one transaction with the data. Every instance of the application asks it
22
22
  * every `access.sourcePollSeconds` and reads the permissions again when it
23
- * differs.
23
+ * differs. Nothing — `undefined`, `null`, an empty string — is a failed
24
+ * reading: it would look like a version that never changes.
24
25
  */
25
- abstract version(): string | Promise<string>;
26
+ abstract version(): PermissionSourceVersion | Promise<PermissionSourceVersion>;
26
27
 
27
28
  /**
28
29
  * The permissions of the prefix, in the form of `definePermissions`. With
@@ -30,3 +31,6 @@ export abstract class PermissionSource {
30
31
  */
31
32
  abstract load(keys?: readonly string[]): PermissionDeclarations | Promise<PermissionDeclarations>;
32
33
  }
34
+
35
+ /** A counter (a number, a bigint) or any non-empty string: the catalog only tells whether it differs. */
36
+ export type PermissionSourceVersion = string | number | bigint;
@@ -4,15 +4,15 @@ import { registerFrameworkClassDeps } from "../../di/module/autoDeps";
4
4
  import { ServiceProvider } from "../../di/ServiceProvider";
5
5
  import { AccessCatalog } from "../catalog/AccessCatalog";
6
6
  import { IAccessCatalog } from "../catalog/IAccessCatalog.service";
7
- import { PermissionSourceLoader } from "../catalog/PermissionSourceLoader";
8
- import { PermissionSources } from "../catalog/PermissionSources";
7
+ import { PermissionSourceLoader } from "../catalog/PermissionSourceLoader.background";
8
+ import { PermissionSourcesService } from "../catalog/PermissionSources.service";
9
9
  import { Access } from "../check/Access";
10
10
  import { RolesAccessPolicy } from "../roles/RolesAccessPolicy";
11
11
 
12
12
  registerFrameworkClassDeps([
13
13
  [Access, [IAccessCatalog, "Config:access"]],
14
- [AccessCatalog, [ServiceProvider, PermissionSources, "Config:access"]],
15
- [PermissionSourceLoader, [PermissionSources]],
16
- [PermissionSources, [ServiceProvider, "Config:access"]],
14
+ [AccessCatalog, [ServiceProvider, PermissionSourcesService, "Config:access"]],
15
+ [PermissionSourceLoader, [PermissionSourcesService]],
16
+ [PermissionSourcesService, [ServiceProvider, "Config:access"]],
17
17
  [RolesAccessPolicy, [IAccessCatalog, "Config:access"]],
18
18
  ]);
@@ -26,7 +26,7 @@ export interface BazisPermissionSources {}
26
26
 
27
27
  export { AccessModule } from "./Access.module";
28
28
  export { definePermissions, type AnyPermissionKey, type AttributeSpec, type OwnPermissionKey, type PermissionDeclarations, type PermissionKey, type PermissionsOf, type PermissionSpec, type ScopeSource, type SourcePermissionKey } from "./catalog/permissions";
29
- export type { PermissionSourceState } from "./catalog/PermissionSources";
29
+ export type { PermissionSourceState } from "./catalog/PermissionSources.service";
30
30
  export { IAccessCatalog } from "./catalog/IAccessCatalog.service";
31
31
  export type { DeclaredRole, OwnedPermission, OwnedReferenceKind } from "./catalog/AccessCatalog";
32
32
  export { IAccess } from "./check/IAccess.service";
@@ -36,7 +36,7 @@ export { can } from "./check/can";
36
36
  export { grantsToWhere, type AccessFilter, type ColumnCondition } from "./check/filter";
37
37
  export { AccessPolicy, type ScopePrincipals } from "./contracts/AccessPolicy";
38
38
  export { AttributeLookup, type LookupColumn } from "./contracts/AttributeLookup";
39
- export { PermissionSource } from "./contracts/PermissionSource";
39
+ export { PermissionSource, type PermissionSourceVersion } from "./contracts/PermissionSource";
40
40
  export { References, type ReferenceContext, type ReferenceItem, type ReferencePage } from "./contracts/References";
41
41
  export { AccessFilterTooLargeError, AccessSetupError } from "./contracts/errors";
42
42
  export type {
@@ -232,7 +232,7 @@ export class CodexAppServerClient implements CodexClient {
232
232
  if (method === "account/updated") this.modelCache = undefined;
233
233
  }, () => { this.clearLogin(); this.modelCache = undefined; });
234
234
  const initialized = await client.request("initialize", {
235
- clientInfo: { name: "bazis", title: "Bazis", version: "0.140.0" }, capabilities: { experimentalApi: true },
235
+ clientInfo: { name: "bazis", title: "Bazis", version: "0.140.1" }, capabilities: { experimentalApi: true },
236
236
  }, 15_000, signal);
237
237
  if (resolve(text(initialized.codexHome, 4096)) !== this.paths.home) throw new CodexError("PROTOCOL_ERROR");
238
238
  client.notify("initialized");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bazis",
3
- "version": "0.140.0",
3
+ "version": "0.140.1",
4
4
  "description": "Modular backend framework for Bun in TypeScript: DI wired by codegen, HTTP, a PostgreSQL ORM, JWT, WebSocket, agents and a CLI.",
5
5
  "license": "MIT",
6
6
  "author": "Alexander Alenin",