@better-forge/contracts 0.0.0-stage → 0.1.0
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/LICENSE +21 -0
- package/README.md +5 -3
- package/dist/apiToken.d.ts +32 -0
- package/dist/apiToken.d.ts.map +1 -0
- package/dist/apiToken.js +36 -0
- package/dist/apiToken.js.map +1 -0
- package/dist/audit.d.ts +22 -0
- package/dist/audit.d.ts.map +1 -0
- package/dist/audit.js +2 -0
- package/dist/audit.js.map +1 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +7 -0
- package/dist/index.js.map +1 -0
- package/dist/locale.d.ts +15 -0
- package/dist/locale.d.ts.map +1 -0
- package/dist/locale.js +5 -0
- package/dist/locale.js.map +1 -0
- package/dist/notification.d.ts +94 -0
- package/dist/notification.d.ts.map +1 -0
- package/dist/notification.js +2 -0
- package/dist/notification.js.map +1 -0
- package/dist/safeStorage.d.ts +9 -0
- package/dist/safeStorage.d.ts.map +1 -0
- package/dist/safeStorage.js +21 -0
- package/dist/safeStorage.js.map +1 -0
- package/dist/vault.d.ts +8 -0
- package/dist/vault.d.ts.map +1 -0
- package/dist/vault.js +2 -0
- package/dist/vault.js.map +1 -0
- package/package.json +43 -6
- package/src/apiToken.ts +44 -0
- package/src/audit.ts +33 -0
- package/src/index.ts +6 -0
- package/src/locale.ts +18 -0
- package/src/notification.ts +110 -0
- package/src/safeStorage.ts +17 -0
- package/src/vault.ts +7 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 BetterStatusPage contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
-
#
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
# @better-forge/contracts
|
|
2
|
+
|
|
3
|
+
Browser-safe types, core translations and a storage wrapper that never throws shared by the other modules.
|
|
4
|
+
|
|
5
|
+
Status: skeleton. Depends on: nothing.
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What an API token may do. A token carries a list of permissions instead of a role: each one names a part of the
|
|
3
|
+
* admin API and whether it may be read (`:read`) or changed (`:write`, which includes reading). `vault:use` is a
|
|
4
|
+
* separate permission to use secrets from a vault in a configuration.
|
|
5
|
+
*/
|
|
6
|
+
export interface TokenResource {
|
|
7
|
+
key: string;
|
|
8
|
+
label: string;
|
|
9
|
+
description: string;
|
|
10
|
+
/** `read` only for parts that cannot be changed through the API. */
|
|
11
|
+
levels: readonly ('read' | 'write')[];
|
|
12
|
+
}
|
|
13
|
+
/** Lets a token set, change or test a configuration that reads a secret from a vault. */
|
|
14
|
+
export declare const VAULT_USE_SCOPE = "vault:use";
|
|
15
|
+
export interface TokenPreset {
|
|
16
|
+
label: string;
|
|
17
|
+
description: string;
|
|
18
|
+
scopes: readonly string[];
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* The permissions a token needs for a request: `<resource>:read` for GET and HEAD, `<resource>:write` for anything
|
|
22
|
+
* else. The vault catalogue is the one part with a single permission.
|
|
23
|
+
*/
|
|
24
|
+
export declare function requiredScope(resource: string, method: string): string;
|
|
25
|
+
/** Whether a token with `granted` may do what needs `required`. Writing a part includes reading it. */
|
|
26
|
+
export declare function tokenAllows(granted: readonly string[], required: string): boolean;
|
|
27
|
+
/**
|
|
28
|
+
* A clean list of permissions out of `known` (reading added wherever writing is granted, ordered as `known` is), or
|
|
29
|
+
* null if one is unknown or none is given.
|
|
30
|
+
*/
|
|
31
|
+
export declare function normalizeScopes(input: unknown, known: readonly string[]): string[] | null;
|
|
32
|
+
//# sourceMappingURL=apiToken.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"apiToken.d.ts","sourceRoot":"","sources":["../src/apiToken.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,MAAM,WAAW,aAAa;IAC5B,GAAG,EAAE,MAAM,CAAA;IACX,KAAK,EAAE,MAAM,CAAA;IACb,WAAW,EAAE,MAAM,CAAA;IACnB,oEAAoE;IACpE,MAAM,EAAE,SAAS,CAAC,MAAM,GAAG,OAAO,CAAC,EAAE,CAAA;CACtC;AAED,yFAAyF;AACzF,eAAO,MAAM,eAAe,cAAc,CAAA;AAE1C,MAAM,WAAW,WAAW;IAAG,KAAK,EAAE,MAAM,CAAC;IAAC,WAAW,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,SAAS,MAAM,EAAE,CAAA;CAAE;AAE9F;;;GAGG;AACH,wBAAgB,aAAa,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,CAGtE;AAED,uGAAuG;AACvG,wBAAgB,WAAW,CAAC,OAAO,EAAE,SAAS,MAAM,EAAE,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAGjF;AAED;;;GAGG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,OAAO,EAAE,KAAK,EAAE,SAAS,MAAM,EAAE,GAAG,MAAM,EAAE,GAAG,IAAI,CAKzF"}
|
package/dist/apiToken.js
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What an API token may do. A token carries a list of permissions instead of a role: each one names a part of the
|
|
3
|
+
* admin API and whether it may be read (`:read`) or changed (`:write`, which includes reading). `vault:use` is a
|
|
4
|
+
* separate permission to use secrets from a vault in a configuration.
|
|
5
|
+
*/
|
|
6
|
+
/** Lets a token set, change or test a configuration that reads a secret from a vault. */
|
|
7
|
+
export const VAULT_USE_SCOPE = 'vault:use';
|
|
8
|
+
/**
|
|
9
|
+
* The permissions a token needs for a request: `<resource>:read` for GET and HEAD, `<resource>:write` for anything
|
|
10
|
+
* else. The vault catalogue is the one part with a single permission.
|
|
11
|
+
*/
|
|
12
|
+
export function requiredScope(resource, method) {
|
|
13
|
+
if (resource === 'vault')
|
|
14
|
+
return VAULT_USE_SCOPE;
|
|
15
|
+
return `${resource}:${method === 'GET' || method === 'HEAD' ? 'read' : 'write'}`;
|
|
16
|
+
}
|
|
17
|
+
/** Whether a token with `granted` may do what needs `required`. Writing a part includes reading it. */
|
|
18
|
+
export function tokenAllows(granted, required) {
|
|
19
|
+
if (granted.includes(required))
|
|
20
|
+
return true;
|
|
21
|
+
return required.endsWith(':read') && granted.includes(`${required.slice(0, -':read'.length)}:write`);
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* A clean list of permissions out of `known` (reading added wherever writing is granted, ordered as `known` is), or
|
|
25
|
+
* null if one is unknown or none is given.
|
|
26
|
+
*/
|
|
27
|
+
export function normalizeScopes(input, known) {
|
|
28
|
+
if (!Array.isArray(input) || input.length === 0 || !input.every((scope) => typeof scope === 'string' && known.includes(scope)))
|
|
29
|
+
return null;
|
|
30
|
+
const scopes = new Set(input);
|
|
31
|
+
for (const scope of input)
|
|
32
|
+
if (scope.endsWith(':write') && known.includes(`${scope.slice(0, -':write'.length)}:read`))
|
|
33
|
+
scopes.add(`${scope.slice(0, -':write'.length)}:read`);
|
|
34
|
+
return known.filter((scope) => scopes.has(scope));
|
|
35
|
+
}
|
|
36
|
+
//# sourceMappingURL=apiToken.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"apiToken.js","sourceRoot":"","sources":["../src/apiToken.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAUH,yFAAyF;AACzF,MAAM,CAAC,MAAM,eAAe,GAAG,WAAW,CAAA;AAI1C;;;GAGG;AACH,MAAM,UAAU,aAAa,CAAC,QAAgB,EAAE,MAAc;IAC5D,IAAI,QAAQ,KAAK,OAAO;QAAE,OAAO,eAAe,CAAA;IAChD,OAAO,GAAG,QAAQ,IAAI,MAAM,KAAK,KAAK,IAAI,MAAM,KAAK,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,OAAO,EAAE,CAAA;AAClF,CAAC;AAED,uGAAuG;AACvG,MAAM,UAAU,WAAW,CAAC,OAA0B,EAAE,QAAgB;IACtE,IAAI,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAC;QAAE,OAAO,IAAI,CAAA;IAC3C,OAAO,QAAQ,CAAC,QAAQ,CAAC,OAAO,CAAC,IAAI,OAAO,CAAC,QAAQ,CAAC,GAAG,QAAQ,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAA;AACtG,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,eAAe,CAAC,KAAc,EAAE,KAAwB;IACtE,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC;QAAE,OAAO,IAAI,CAAA;IAC3I,MAAM,MAAM,GAAG,IAAI,GAAG,CAAS,KAAiB,CAAC,CAAA;IACjD,KAAK,MAAM,KAAK,IAAI,KAAiB;QAAE,IAAI,KAAK,CAAC,QAAQ,CAAC,QAAQ,CAAC,IAAI,KAAK,CAAC,QAAQ,CAAC,GAAG,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,QAAQ,CAAC,MAAM,CAAC,OAAO,CAAC;YAAE,MAAM,CAAC,GAAG,CAAC,GAAG,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,QAAQ,CAAC,MAAM,CAAC,OAAO,CAAC,CAAA;IACzL,OAAO,KAAK,CAAC,MAAM,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,MAAM,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,CAAA;AACnD,CAAC"}
|
package/dist/audit.d.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/** `allow` records a successful sign-in; `deny` a refused action, such as a sign-in with a wrong password. */
|
|
2
|
+
export type AuditAction = 'create' | 'update' | 'delete' | 'allow' | 'deny';
|
|
3
|
+
/** The kinds of object the shared modules write to the audit log; an app adds its own. */
|
|
4
|
+
export type KitAuditEntityType = 'notification_channel' | 'smtp_settings' | 'vault' | 'vault_secret' | 'user' | 'api_token' | 'oidc_settings'
|
|
5
|
+
/** Security changes on a user account: password, two-factor, SSO link. */
|
|
6
|
+
| 'user-security'
|
|
7
|
+
/** A configuration import; the entities it changed have entries of their own. */
|
|
8
|
+
| 'config' | 'backup' | 'backup-config' | 'notification_delivery'
|
|
9
|
+
/** Every sign-in attempt, with a password or SSO; `diff.method` says which. */
|
|
10
|
+
| 'sign_in';
|
|
11
|
+
export interface AuditLogEntry<EntityType extends string = string> {
|
|
12
|
+
id: number;
|
|
13
|
+
userId: number;
|
|
14
|
+
userEmail: string;
|
|
15
|
+
action: AuditAction;
|
|
16
|
+
entityType: EntityType;
|
|
17
|
+
entityId: string | null;
|
|
18
|
+
entityName: string;
|
|
19
|
+
diff: Record<string, unknown> | null;
|
|
20
|
+
timestamp: number;
|
|
21
|
+
}
|
|
22
|
+
//# sourceMappingURL=audit.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"audit.d.ts","sourceRoot":"","sources":["../src/audit.ts"],"names":[],"mappings":"AAAA,8GAA8G;AAC9G,MAAM,MAAM,WAAW,GAAG,QAAQ,GAAG,QAAQ,GAAG,QAAQ,GAAG,OAAO,GAAG,MAAM,CAAA;AAE3E,0FAA0F;AAC1F,MAAM,MAAM,kBAAkB,GAC1B,sBAAsB,GACtB,eAAe,GACf,OAAO,GACP,cAAc,GACd,MAAM,GACN,WAAW,GACX,eAAe;AACjB,0EAA0E;GACxE,eAAe;AACjB,iFAAiF;GAC/E,QAAQ,GACR,QAAQ,GACR,eAAe,GACf,uBAAuB;AACzB,+EAA+E;GAC7E,SAAS,CAAA;AAEb,MAAM,WAAW,aAAa,CAAC,UAAU,SAAS,MAAM,GAAG,MAAM;IAC/D,EAAE,EAAE,MAAM,CAAA;IACV,MAAM,EAAE,MAAM,CAAA;IACd,SAAS,EAAE,MAAM,CAAA;IACjB,MAAM,EAAE,WAAW,CAAA;IACnB,UAAU,EAAE,UAAU,CAAA;IACtB,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAA;IACvB,UAAU,EAAE,MAAM,CAAA;IAClB,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAA;IACpC,SAAS,EAAE,MAAM,CAAA;CAClB"}
|
package/dist/audit.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"audit.js","sourceRoot":"","sources":["../src/audit.ts"],"names":[],"mappings":""}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,YAAY,CAAA;AAC1B,cAAc,mBAAmB,CAAA;AACjC,cAAc,eAAe,CAAA;AAC7B,cAAc,YAAY,CAAA;AAC1B,cAAc,aAAa,CAAA;AAC3B,cAAc,kBAAkB,CAAA"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,YAAY,CAAA;AAC1B,cAAc,mBAAmB,CAAA;AACjC,cAAc,eAAe,CAAA;AAC7B,cAAc,YAAY,CAAA;AAC1B,cAAc,aAAa,CAAA;AAC3B,cAAc,kBAAkB,CAAA"}
|
package/dist/locale.d.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export interface Locale<Key extends string = string> {
|
|
2
|
+
code: string;
|
|
3
|
+
name: string;
|
|
4
|
+
isDefault: number;
|
|
5
|
+
translations: Partial<Record<Key, string>>;
|
|
6
|
+
updatedAt: number;
|
|
7
|
+
}
|
|
8
|
+
export interface LocaleSummary {
|
|
9
|
+
code: string;
|
|
10
|
+
name: string;
|
|
11
|
+
isDefault?: number;
|
|
12
|
+
}
|
|
13
|
+
/** The language part of a locale code: "pl-PL" and "pl_PL" are both "pl". */
|
|
14
|
+
export declare function baseLanguage(code: string): string;
|
|
15
|
+
//# sourceMappingURL=locale.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"locale.d.ts","sourceRoot":"","sources":["../src/locale.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,MAAM,CAAC,GAAG,SAAS,MAAM,GAAG,MAAM;IACjD,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,EAAE,MAAM,CAAA;IACZ,SAAS,EAAE,MAAM,CAAA;IACjB,YAAY,EAAE,OAAO,CAAC,MAAM,CAAC,GAAG,EAAE,MAAM,CAAC,CAAC,CAAA;IAC1C,SAAS,EAAE,MAAM,CAAA;CAClB;AAED,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,EAAE,MAAM,CAAA;IACZ,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AAED,6EAA6E;AAC7E,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAEjD"}
|
package/dist/locale.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"locale.js","sourceRoot":"","sources":["../src/locale.ts"],"names":[],"mappings":"AAcA,6EAA6E;AAC7E,MAAM,UAAU,YAAY,CAAC,IAAY;IACvC,OAAO,IAAI,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAA;AACzD,CAAC"}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import type { VaultRef } from './vault.js';
|
|
2
|
+
export type NotificationChannelType = 'email' | 'webhook' | 'discord' | 'teams' | 'slack' | 'telegram' | 'ntfy' | 'pushover';
|
|
3
|
+
export interface EmailNotificationConfig {
|
|
4
|
+
to: string;
|
|
5
|
+
subject: string;
|
|
6
|
+
body: string;
|
|
7
|
+
}
|
|
8
|
+
export interface WebhookNotificationConfig {
|
|
9
|
+
url: string;
|
|
10
|
+
method: 'GET' | 'POST' | 'PUT' | 'PATCH';
|
|
11
|
+
headers?: Record<string, string>;
|
|
12
|
+
body?: string;
|
|
13
|
+
}
|
|
14
|
+
export interface DiscordNotificationConfig {
|
|
15
|
+
webhookUrl: string;
|
|
16
|
+
username?: string;
|
|
17
|
+
avatarUrl?: string;
|
|
18
|
+
content?: string;
|
|
19
|
+
}
|
|
20
|
+
export interface TeamsNotificationConfig {
|
|
21
|
+
webhookUrl: string;
|
|
22
|
+
summary?: string;
|
|
23
|
+
}
|
|
24
|
+
export interface SlackNotificationConfig {
|
|
25
|
+
webhookUrl: string;
|
|
26
|
+
text?: string;
|
|
27
|
+
}
|
|
28
|
+
export interface TelegramNotificationConfig {
|
|
29
|
+
/** Direct token; the API only ever returns it masked. Ignored when `vault` is set. */
|
|
30
|
+
botToken?: string;
|
|
31
|
+
/** Vault secret holding the token: a `value` secret. */
|
|
32
|
+
vault?: VaultRef;
|
|
33
|
+
chatId: string;
|
|
34
|
+
text?: string;
|
|
35
|
+
}
|
|
36
|
+
export interface NtfyNotificationConfig {
|
|
37
|
+
/** Server root; ntfy.sh when omitted. */
|
|
38
|
+
serverUrl?: string;
|
|
39
|
+
topic: string;
|
|
40
|
+
/** Access token of a protected topic; the API only ever returns it masked. Ignored when `vault` is set. */
|
|
41
|
+
accessToken?: string;
|
|
42
|
+
/** Vault secret holding the access token: a `value` secret. */
|
|
43
|
+
vault?: VaultRef;
|
|
44
|
+
text?: string;
|
|
45
|
+
}
|
|
46
|
+
export interface PushoverNotificationConfig {
|
|
47
|
+
/** Application token; the API only ever returns it masked. Ignored when `vault` is set. */
|
|
48
|
+
appToken?: string;
|
|
49
|
+
/** Vault secret holding the application token: a `value` secret. */
|
|
50
|
+
vault?: VaultRef;
|
|
51
|
+
/** User or group key that receives the messages. */
|
|
52
|
+
userKey: string;
|
|
53
|
+
text?: string;
|
|
54
|
+
}
|
|
55
|
+
/** The wording a channel has for one kind of event, by event type: only the message fields of its type (subject, body, text...). */
|
|
56
|
+
export interface ChannelEventOverrides {
|
|
57
|
+
eventOverrides?: Record<string, Record<string, string>>;
|
|
58
|
+
}
|
|
59
|
+
export type NotificationChannelConfig = (EmailNotificationConfig | WebhookNotificationConfig | DiscordNotificationConfig | TeamsNotificationConfig | SlackNotificationConfig | TelegramNotificationConfig | NtfyNotificationConfig | PushoverNotificationConfig) & ChannelEventOverrides;
|
|
60
|
+
/** Quiet hours silence a channel during a recurring local-time window. */
|
|
61
|
+
export interface QuietHoursPolicy {
|
|
62
|
+
enabled: boolean;
|
|
63
|
+
/** Local start of the quiet window, `HH:MM` (24h). */
|
|
64
|
+
start: string;
|
|
65
|
+
/** Local end of the quiet window, `HH:MM`. When <= start the window wraps past midnight. */
|
|
66
|
+
end: string;
|
|
67
|
+
/** IANA timezone the window is expressed in, e.g. `Europe/Warsaw`. */
|
|
68
|
+
timezone: string;
|
|
69
|
+
/** `defer` holds notifications until the window ends; `suppress` drops them. */
|
|
70
|
+
mode: 'defer' | 'suppress';
|
|
71
|
+
}
|
|
72
|
+
export type NotificationSuppressionReason = 'quiet-hours' | 'throttled' | 'grouped';
|
|
73
|
+
export interface SmtpSettings {
|
|
74
|
+
host: string;
|
|
75
|
+
port: number;
|
|
76
|
+
secure: number;
|
|
77
|
+
user: string;
|
|
78
|
+
password: string;
|
|
79
|
+
fromAddress: string;
|
|
80
|
+
fromName: string;
|
|
81
|
+
vault?: VaultRef | null;
|
|
82
|
+
updatedAt: number;
|
|
83
|
+
}
|
|
84
|
+
export type NotificationDeliveryStatus = 'pending' | 'delivered' | 'failed' | 'suppressed';
|
|
85
|
+
export interface NotificationDeliveryAttempt {
|
|
86
|
+
id: number;
|
|
87
|
+
deliveryId: number;
|
|
88
|
+
attemptNumber: number;
|
|
89
|
+
status: 'delivered' | 'failed';
|
|
90
|
+
error: string | null;
|
|
91
|
+
startedAt: number;
|
|
92
|
+
completedAt: number;
|
|
93
|
+
}
|
|
94
|
+
//# sourceMappingURL=notification.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"notification.d.ts","sourceRoot":"","sources":["../src/notification.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"notification.js","sourceRoot":"","sources":["../src/notification.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/** Browser storage that never throws: a blocked or full storage reads as empty and drops writes. */
|
|
2
|
+
export interface SafeStorage {
|
|
3
|
+
get(key: string): string | null;
|
|
4
|
+
set(key: string, value: string): void;
|
|
5
|
+
remove(key: string): void;
|
|
6
|
+
}
|
|
7
|
+
export declare const safeLocalStorage: SafeStorage;
|
|
8
|
+
export declare const safeSessionStorage: SafeStorage;
|
|
9
|
+
//# sourceMappingURL=safeStorage.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"safeStorage.d.ts","sourceRoot":"","sources":["../src/safeStorage.ts"],"names":[],"mappings":"AAAA,oGAAoG;AACpG,MAAM,WAAW,WAAW;IAC1B,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAAA;IAC/B,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI,CAAA;IACrC,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAAA;CAC1B;AAUD,eAAO,MAAM,gBAAgB,aAA2B,CAAA;AACxD,eAAO,MAAM,kBAAkB,aAA6B,CAAA"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
function safe(area) {
|
|
2
|
+
return {
|
|
3
|
+
get(key) { try {
|
|
4
|
+
return area().getItem(key);
|
|
5
|
+
}
|
|
6
|
+
catch {
|
|
7
|
+
return null;
|
|
8
|
+
} },
|
|
9
|
+
set(key, value) { try {
|
|
10
|
+
area().setItem(key, value);
|
|
11
|
+
}
|
|
12
|
+
catch { /* storage unavailable: the value lasts for this page only */ } },
|
|
13
|
+
remove(key) { try {
|
|
14
|
+
area().removeItem(key);
|
|
15
|
+
}
|
|
16
|
+
catch { /* storage unavailable: nothing to remove */ } },
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
export const safeLocalStorage = safe(() => localStorage);
|
|
20
|
+
export const safeSessionStorage = safe(() => sessionStorage);
|
|
21
|
+
//# sourceMappingURL=safeStorage.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"safeStorage.js","sourceRoot":"","sources":["../src/safeStorage.ts"],"names":[],"mappings":"AAOA,SAAS,IAAI,CAAC,IAAmB;IAC/B,OAAO;QACL,GAAG,CAAC,GAAG,IAAI,IAAI,CAAC;YAAC,OAAO,IAAI,EAAE,CAAC,OAAO,CAAC,GAAG,CAAC,CAAA;QAAC,CAAC;QAAC,MAAM,CAAC;YAAC,OAAO,IAAI,CAAA;QAAC,CAAC,CAAC,CAAC;QACrE,GAAG,CAAC,GAAG,EAAE,KAAK,IAAI,IAAI,CAAC;YAAC,IAAI,EAAE,CAAC,OAAO,CAAC,GAAG,EAAE,KAAK,CAAC,CAAA;QAAC,CAAC;QAAC,MAAM,CAAC,CAAC,6DAA6D,CAAC,CAAC,CAAC,CAAC;QAC9H,MAAM,CAAC,GAAG,IAAI,IAAI,CAAC;YAAC,IAAI,EAAE,CAAC,UAAU,CAAC,GAAG,CAAC,CAAA;QAAC,CAAC;QAAC,MAAM,CAAC,CAAC,4CAA4C,CAAC,CAAC,CAAC,CAAC;KACtG,CAAA;AACH,CAAC;AAED,MAAM,CAAC,MAAM,gBAAgB,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC,YAAY,CAAC,CAAA;AACxD,MAAM,CAAC,MAAM,kBAAkB,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC,cAAc,CAAC,CAAA"}
|
package/dist/vault.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/** Reference to a vault secret, with optional field mapping for json-type secrets */
|
|
2
|
+
export interface VaultRef {
|
|
3
|
+
vaultId: number;
|
|
4
|
+
secretId: number;
|
|
5
|
+
/** Only for json-type secrets: maps our field names to JSON object keys */
|
|
6
|
+
fieldMapping?: Record<string, string>;
|
|
7
|
+
}
|
|
8
|
+
//# sourceMappingURL=vault.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"vault.d.ts","sourceRoot":"","sources":["../src/vault.ts"],"names":[],"mappings":"AAAA,qFAAqF;AACrF,MAAM,WAAW,QAAQ;IACvB,OAAO,EAAE,MAAM,CAAA;IACf,QAAQ,EAAE,MAAM,CAAA;IAChB,2EAA2E;IAC3E,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CACtC"}
|
package/dist/vault.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"vault.js","sourceRoot":"","sources":["../src/vault.ts"],"names":[],"mappings":""}
|
package/package.json
CHANGED
|
@@ -1,6 +1,43 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@better-forge/contracts",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
1
|
+
{
|
|
2
|
+
"name": "@better-forge/contracts",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Browser-safe types, core translations and a storage wrapper that never throws.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/BElluu/Better-Forge.git",
|
|
9
|
+
"directory": "packages/contracts"
|
|
10
|
+
},
|
|
11
|
+
"homepage": "https://github.com/BElluu/Better-Forge/tree/main/packages/contracts#readme",
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/BElluu/Better-Forge/issues"
|
|
14
|
+
},
|
|
15
|
+
"publishConfig": {
|
|
16
|
+
"access": "public"
|
|
17
|
+
},
|
|
18
|
+
"type": "module",
|
|
19
|
+
"main": "./dist/index.js",
|
|
20
|
+
"types": "./dist/index.d.ts",
|
|
21
|
+
"exports": {
|
|
22
|
+
".": {
|
|
23
|
+
"types": "./dist/index.d.ts",
|
|
24
|
+
"import": "./dist/index.js"
|
|
25
|
+
},
|
|
26
|
+
"./package.json": "./package.json"
|
|
27
|
+
},
|
|
28
|
+
"files": [
|
|
29
|
+
"dist",
|
|
30
|
+
"src",
|
|
31
|
+
"!src/**/*.test.*"
|
|
32
|
+
],
|
|
33
|
+
"scripts": {
|
|
34
|
+
"build": "tsc -p tsconfig.json",
|
|
35
|
+
"dev": "tsc -p tsconfig.json --watch --preserveWatchOutput"
|
|
36
|
+
},
|
|
37
|
+
"devDependencies": {
|
|
38
|
+
"typescript": "^5.7.3"
|
|
39
|
+
},
|
|
40
|
+
"engines": {
|
|
41
|
+
"node": ">=26.10.0"
|
|
42
|
+
}
|
|
43
|
+
}
|
package/src/apiToken.ts
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What an API token may do. A token carries a list of permissions instead of a role: each one names a part of the
|
|
3
|
+
* admin API and whether it may be read (`:read`) or changed (`:write`, which includes reading). `vault:use` is a
|
|
4
|
+
* separate permission to use secrets from a vault in a configuration.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export interface TokenResource {
|
|
8
|
+
key: string
|
|
9
|
+
label: string
|
|
10
|
+
description: string
|
|
11
|
+
/** `read` only for parts that cannot be changed through the API. */
|
|
12
|
+
levels: readonly ('read' | 'write')[]
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** Lets a token set, change or test a configuration that reads a secret from a vault. */
|
|
16
|
+
export const VAULT_USE_SCOPE = 'vault:use'
|
|
17
|
+
|
|
18
|
+
export interface TokenPreset { label: string; description: string; scopes: readonly string[] }
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* The permissions a token needs for a request: `<resource>:read` for GET and HEAD, `<resource>:write` for anything
|
|
22
|
+
* else. The vault catalogue is the one part with a single permission.
|
|
23
|
+
*/
|
|
24
|
+
export function requiredScope(resource: string, method: string): string {
|
|
25
|
+
if (resource === 'vault') return VAULT_USE_SCOPE
|
|
26
|
+
return `${resource}:${method === 'GET' || method === 'HEAD' ? 'read' : 'write'}`
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Whether a token with `granted` may do what needs `required`. Writing a part includes reading it. */
|
|
30
|
+
export function tokenAllows(granted: readonly string[], required: string): boolean {
|
|
31
|
+
if (granted.includes(required)) return true
|
|
32
|
+
return required.endsWith(':read') && granted.includes(`${required.slice(0, -':read'.length)}:write`)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* A clean list of permissions out of `known` (reading added wherever writing is granted, ordered as `known` is), or
|
|
37
|
+
* null if one is unknown or none is given.
|
|
38
|
+
*/
|
|
39
|
+
export function normalizeScopes(input: unknown, known: readonly string[]): string[] | null {
|
|
40
|
+
if (!Array.isArray(input) || input.length === 0 || !input.every((scope) => typeof scope === 'string' && known.includes(scope))) return null
|
|
41
|
+
const scopes = new Set<string>(input as string[])
|
|
42
|
+
for (const scope of input as string[]) if (scope.endsWith(':write') && known.includes(`${scope.slice(0, -':write'.length)}:read`)) scopes.add(`${scope.slice(0, -':write'.length)}:read`)
|
|
43
|
+
return known.filter((scope) => scopes.has(scope))
|
|
44
|
+
}
|
package/src/audit.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/** `allow` records a successful sign-in; `deny` a refused action, such as a sign-in with a wrong password. */
|
|
2
|
+
export type AuditAction = 'create' | 'update' | 'delete' | 'allow' | 'deny'
|
|
3
|
+
|
|
4
|
+
/** The kinds of object the shared modules write to the audit log; an app adds its own. */
|
|
5
|
+
export type KitAuditEntityType =
|
|
6
|
+
| 'notification_channel'
|
|
7
|
+
| 'smtp_settings'
|
|
8
|
+
| 'vault'
|
|
9
|
+
| 'vault_secret'
|
|
10
|
+
| 'user'
|
|
11
|
+
| 'api_token'
|
|
12
|
+
| 'oidc_settings'
|
|
13
|
+
/** Security changes on a user account: password, two-factor, SSO link. */
|
|
14
|
+
| 'user-security'
|
|
15
|
+
/** A configuration import; the entities it changed have entries of their own. */
|
|
16
|
+
| 'config'
|
|
17
|
+
| 'backup'
|
|
18
|
+
| 'backup-config'
|
|
19
|
+
| 'notification_delivery'
|
|
20
|
+
/** Every sign-in attempt, with a password or SSO; `diff.method` says which. */
|
|
21
|
+
| 'sign_in'
|
|
22
|
+
|
|
23
|
+
export interface AuditLogEntry<EntityType extends string = string> {
|
|
24
|
+
id: number
|
|
25
|
+
userId: number
|
|
26
|
+
userEmail: string
|
|
27
|
+
action: AuditAction
|
|
28
|
+
entityType: EntityType
|
|
29
|
+
entityId: string | null
|
|
30
|
+
entityName: string
|
|
31
|
+
diff: Record<string, unknown> | null
|
|
32
|
+
timestamp: number
|
|
33
|
+
}
|
package/src/index.ts
ADDED
package/src/locale.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export interface Locale<Key extends string = string> {
|
|
2
|
+
code: string
|
|
3
|
+
name: string
|
|
4
|
+
isDefault: number
|
|
5
|
+
translations: Partial<Record<Key, string>>
|
|
6
|
+
updatedAt: number
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface LocaleSummary {
|
|
10
|
+
code: string
|
|
11
|
+
name: string
|
|
12
|
+
isDefault?: number
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** The language part of a locale code: "pl-PL" and "pl_PL" are both "pl". */
|
|
16
|
+
export function baseLanguage(code: string): string {
|
|
17
|
+
return code.trim().toLowerCase().split(/[-_]/)[0] ?? ''
|
|
18
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import type { VaultRef } from './vault.js'
|
|
2
|
+
|
|
3
|
+
export type NotificationChannelType = 'email' | 'webhook' | 'discord' | 'teams' | 'slack' | 'telegram' | 'ntfy' | 'pushover'
|
|
4
|
+
|
|
5
|
+
export interface EmailNotificationConfig {
|
|
6
|
+
to: string
|
|
7
|
+
subject: string
|
|
8
|
+
body: string
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface WebhookNotificationConfig {
|
|
12
|
+
url: string
|
|
13
|
+
method: 'GET' | 'POST' | 'PUT' | 'PATCH'
|
|
14
|
+
headers?: Record<string, string>
|
|
15
|
+
body?: string
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface DiscordNotificationConfig {
|
|
19
|
+
webhookUrl: string
|
|
20
|
+
username?: string
|
|
21
|
+
avatarUrl?: string
|
|
22
|
+
content?: string
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface TeamsNotificationConfig {
|
|
26
|
+
webhookUrl: string
|
|
27
|
+
summary?: string
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface SlackNotificationConfig {
|
|
31
|
+
webhookUrl: string
|
|
32
|
+
text?: string
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface TelegramNotificationConfig {
|
|
36
|
+
/** Direct token; the API only ever returns it masked. Ignored when `vault` is set. */
|
|
37
|
+
botToken?: string
|
|
38
|
+
/** Vault secret holding the token: a `value` secret. */
|
|
39
|
+
vault?: VaultRef
|
|
40
|
+
chatId: string
|
|
41
|
+
text?: string
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface NtfyNotificationConfig {
|
|
45
|
+
/** Server root; ntfy.sh when omitted. */
|
|
46
|
+
serverUrl?: string
|
|
47
|
+
topic: string
|
|
48
|
+
/** Access token of a protected topic; the API only ever returns it masked. Ignored when `vault` is set. */
|
|
49
|
+
accessToken?: string
|
|
50
|
+
/** Vault secret holding the access token: a `value` secret. */
|
|
51
|
+
vault?: VaultRef
|
|
52
|
+
text?: string
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface PushoverNotificationConfig {
|
|
56
|
+
/** Application token; the API only ever returns it masked. Ignored when `vault` is set. */
|
|
57
|
+
appToken?: string
|
|
58
|
+
/** Vault secret holding the application token: a `value` secret. */
|
|
59
|
+
vault?: VaultRef
|
|
60
|
+
/** User or group key that receives the messages. */
|
|
61
|
+
userKey: string
|
|
62
|
+
text?: string
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** The wording a channel has for one kind of event, by event type: only the message fields of its type (subject, body, text...). */
|
|
66
|
+
export interface ChannelEventOverrides {
|
|
67
|
+
eventOverrides?: Record<string, Record<string, string>>
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export type NotificationChannelConfig = (EmailNotificationConfig | WebhookNotificationConfig | DiscordNotificationConfig | TeamsNotificationConfig | SlackNotificationConfig | TelegramNotificationConfig | NtfyNotificationConfig | PushoverNotificationConfig) & ChannelEventOverrides
|
|
71
|
+
|
|
72
|
+
/** Quiet hours silence a channel during a recurring local-time window. */
|
|
73
|
+
export interface QuietHoursPolicy {
|
|
74
|
+
enabled: boolean
|
|
75
|
+
/** Local start of the quiet window, `HH:MM` (24h). */
|
|
76
|
+
start: string
|
|
77
|
+
/** Local end of the quiet window, `HH:MM`. When <= start the window wraps past midnight. */
|
|
78
|
+
end: string
|
|
79
|
+
/** IANA timezone the window is expressed in, e.g. `Europe/Warsaw`. */
|
|
80
|
+
timezone: string
|
|
81
|
+
/** `defer` holds notifications until the window ends; `suppress` drops them. */
|
|
82
|
+
mode: 'defer' | 'suppress'
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export type NotificationSuppressionReason = 'quiet-hours' | 'throttled' | 'grouped'
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
export interface SmtpSettings {
|
|
89
|
+
host: string
|
|
90
|
+
port: number
|
|
91
|
+
secure: number
|
|
92
|
+
user: string
|
|
93
|
+
password: string
|
|
94
|
+
fromAddress: string
|
|
95
|
+
fromName: string
|
|
96
|
+
vault?: VaultRef | null
|
|
97
|
+
updatedAt: number
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export type NotificationDeliveryStatus = 'pending' | 'delivered' | 'failed' | 'suppressed'
|
|
101
|
+
|
|
102
|
+
export interface NotificationDeliveryAttempt {
|
|
103
|
+
id: number
|
|
104
|
+
deliveryId: number
|
|
105
|
+
attemptNumber: number
|
|
106
|
+
status: 'delivered' | 'failed'
|
|
107
|
+
error: string | null
|
|
108
|
+
startedAt: number
|
|
109
|
+
completedAt: number
|
|
110
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/** Browser storage that never throws: a blocked or full storage reads as empty and drops writes. */
|
|
2
|
+
export interface SafeStorage {
|
|
3
|
+
get(key: string): string | null
|
|
4
|
+
set(key: string, value: string): void
|
|
5
|
+
remove(key: string): void
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function safe(area: () => Storage): SafeStorage {
|
|
9
|
+
return {
|
|
10
|
+
get(key) { try { return area().getItem(key) } catch { return null } },
|
|
11
|
+
set(key, value) { try { area().setItem(key, value) } catch { /* storage unavailable: the value lasts for this page only */ } },
|
|
12
|
+
remove(key) { try { area().removeItem(key) } catch { /* storage unavailable: nothing to remove */ } },
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export const safeLocalStorage = safe(() => localStorage)
|
|
17
|
+
export const safeSessionStorage = safe(() => sessionStorage)
|
package/src/vault.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/** Reference to a vault secret, with optional field mapping for json-type secrets */
|
|
2
|
+
export interface VaultRef {
|
|
3
|
+
vaultId: number
|
|
4
|
+
secretId: number
|
|
5
|
+
/** Only for json-type secrets: maps our field names to JSON object keys */
|
|
6
|
+
fieldMapping?: Record<string, string>
|
|
7
|
+
}
|