@digimuza/system-core 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/INTEGRATION.md +191 -0
- package/README.md +156 -2
- package/dist/config/browser.d.ts +17 -0
- package/dist/config/browser.d.ts.map +1 -0
- package/dist/config/browser.js +16 -0
- package/dist/config/browser.js.map +1 -0
- package/dist/config/index.d.ts +71 -0
- package/dist/config/index.d.ts.map +1 -0
- package/dist/config/index.js +166 -0
- package/dist/config/index.js.map +1 -0
- package/dist/convex/functions.d.ts +399 -0
- package/dist/convex/functions.d.ts.map +1 -0
- package/dist/convex/functions.js +269 -0
- package/dist/convex/functions.js.map +1 -0
- package/dist/convex/index.d.ts +7 -0
- package/dist/convex/index.d.ts.map +1 -0
- package/dist/convex/index.js +9 -0
- package/dist/convex/index.js.map +1 -0
- package/dist/convex/schema.d.ts +338 -0
- package/dist/convex/schema.d.ts.map +1 -0
- package/dist/convex/schema.js +155 -0
- package/dist/convex/schema.js.map +1 -0
- package/dist/convex/store.d.ts +10 -0
- package/dist/convex/store.d.ts.map +1 -0
- package/dist/convex/store.js +62 -0
- package/dist/convex/store.js.map +1 -0
- package/dist/core/access.d.ts +96 -0
- package/dist/core/access.d.ts.map +1 -0
- package/dist/core/access.js +266 -0
- package/dist/core/access.js.map +1 -0
- package/dist/core/api-keys.d.ts +39 -0
- package/dist/core/api-keys.d.ts.map +1 -0
- package/dist/core/api-keys.js +115 -0
- package/dist/core/api-keys.js.map +1 -0
- package/dist/core/audit.d.ts +30 -0
- package/dist/core/audit.d.ts.map +1 -0
- package/dist/core/audit.js +66 -0
- package/dist/core/audit.js.map +1 -0
- package/dist/core/catalog.d.ts +123 -0
- package/dist/core/catalog.d.ts.map +1 -0
- package/dist/core/catalog.js +157 -0
- package/dist/core/catalog.js.map +1 -0
- package/dist/core/context.d.ts +41 -0
- package/dist/core/context.d.ts.map +1 -0
- package/dist/core/context.js +34 -0
- package/dist/core/context.js.map +1 -0
- package/dist/core/crypto.d.ts +8 -0
- package/dist/core/crypto.d.ts.map +1 -0
- package/dist/core/crypto.js +41 -0
- package/dist/core/crypto.js.map +1 -0
- package/dist/core/errors.d.ts +46 -0
- package/dist/core/errors.d.ts.map +1 -0
- package/dist/core/errors.js +77 -0
- package/dist/core/errors.js.map +1 -0
- package/dist/core/feature-flags.d.ts +26 -0
- package/dist/core/feature-flags.d.ts.map +1 -0
- package/dist/core/feature-flags.js +66 -0
- package/dist/core/feature-flags.js.map +1 -0
- package/dist/core/index.d.ts +18 -0
- package/dist/core/index.d.ts.map +1 -0
- package/dist/core/index.js +20 -0
- package/dist/core/index.js.map +1 -0
- package/dist/core/invites.d.ts +70 -0
- package/dist/core/invites.d.ts.map +1 -0
- package/dist/core/invites.js +180 -0
- package/dist/core/invites.js.map +1 -0
- package/dist/core/jobs.d.ts +38 -0
- package/dist/core/jobs.d.ts.map +1 -0
- package/dist/core/jobs.js +52 -0
- package/dist/core/jobs.js.map +1 -0
- package/dist/core/modules-service.d.ts +52 -0
- package/dist/core/modules-service.d.ts.map +1 -0
- package/dist/core/modules-service.js +75 -0
- package/dist/core/modules-service.js.map +1 -0
- package/dist/core/redact.d.ts +9 -0
- package/dist/core/redact.d.ts.map +1 -0
- package/dist/core/redact.js +58 -0
- package/dist/core/redact.js.map +1 -0
- package/dist/core/registry.d.ts +36 -0
- package/dist/core/registry.d.ts.map +1 -0
- package/dist/core/registry.js +170 -0
- package/dist/core/registry.js.map +1 -0
- package/dist/core/roles.d.ts +122 -0
- package/dist/core/roles.d.ts.map +1 -0
- package/dist/core/roles.js +426 -0
- package/dist/core/roles.js.map +1 -0
- package/dist/core/store.d.ts +231 -0
- package/dist/core/store.d.ts.map +1 -0
- package/dist/core/store.js +33 -0
- package/dist/core/store.js.map +1 -0
- package/dist/core/system-modules.d.ts +44 -0
- package/dist/core/system-modules.d.ts.map +1 -0
- package/dist/core/system-modules.js +205 -0
- package/dist/core/system-modules.js.map +1 -0
- package/dist/core/users.d.ts +64 -0
- package/dist/core/users.d.ts.map +1 -0
- package/dist/core/users.js +184 -0
- package/dist/core/users.js.map +1 -0
- package/dist/diagnostics/logging.d.ts +44 -0
- package/dist/diagnostics/logging.d.ts.map +1 -0
- package/dist/diagnostics/logging.js +64 -0
- package/dist/diagnostics/logging.js.map +1 -0
- package/dist/diagnostics/tracing.d.ts +65 -0
- package/dist/diagnostics/tracing.d.ts.map +1 -0
- package/dist/diagnostics/tracing.js +110 -0
- package/dist/diagnostics/tracing.js.map +1 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +7 -0
- package/dist/index.js.map +1 -0
- package/dist/modules/index.d.ts +4 -0
- package/dist/modules/index.d.ts.map +1 -0
- package/dist/modules/index.js +5 -0
- package/dist/modules/index.js.map +1 -0
- package/dist/next/index.d.ts +57 -0
- package/dist/next/index.d.ts.map +1 -0
- package/dist/next/index.js +97 -0
- package/dist/next/index.js.map +1 -0
- package/dist/react/index.d.ts +51 -0
- package/dist/react/index.d.ts.map +1 -0
- package/dist/react/index.js +53 -0
- package/dist/react/index.js.map +1 -0
- package/dist/testing/index.d.ts +33 -0
- package/dist/testing/index.d.ts.map +1 -0
- package/dist/testing/index.js +94 -0
- package/dist/testing/index.js.map +1 -0
- package/package.json +110 -4
- package/src/config/browser.ts +16 -0
- package/src/config/index.ts +216 -0
- package/src/convex/functions.ts +371 -0
- package/src/convex/index.ts +9 -0
- package/src/convex/schema.ts +172 -0
- package/src/convex/store.ts +68 -0
- package/src/core/access.ts +307 -0
- package/src/core/api-keys.ts +141 -0
- package/src/core/audit.ts +88 -0
- package/src/core/catalog.ts +226 -0
- package/src/core/context.ts +73 -0
- package/src/core/crypto.ts +43 -0
- package/src/core/errors.ts +97 -0
- package/src/core/feature-flags.ts +70 -0
- package/src/core/index.ts +19 -0
- package/src/core/invites.ts +198 -0
- package/src/core/jobs.ts +58 -0
- package/src/core/modules-service.ts +111 -0
- package/src/core/redact.ts +56 -0
- package/src/core/registry.ts +176 -0
- package/src/core/roles.ts +431 -0
- package/src/core/store.ts +211 -0
- package/src/core/system-modules.ts +210 -0
- package/src/core/users.ts +210 -0
- package/src/diagnostics/logging.ts +99 -0
- package/src/diagnostics/tracing.ts +171 -0
- package/src/index.ts +19 -0
- package/src/modules/index.ts +18 -0
- package/src/next/index.ts +144 -0
- package/src/react/index.tsx +75 -0
- package/src/testing/index.ts +100 -0
package/INTEGRATION.md
ADDED
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
# Integrating `@digimuza/system-core`
|
|
2
|
+
|
|
3
|
+
`README.md` is the product contract. This guide shows how to wire the package
|
|
4
|
+
into one tenant's Next.js + Convex application.
|
|
5
|
+
|
|
6
|
+
## Install
|
|
7
|
+
|
|
8
|
+
```bash
|
|
9
|
+
npm install @digimuza/system-core convex zod # + next, react for the Next.js entries
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
Pin the version per tenant app (`"@digimuza/system-core": "0.1.0"`, or `~0.1.0`) and upgrade deliberately.
|
|
13
|
+
|
|
14
|
+
## Entry points
|
|
15
|
+
|
|
16
|
+
| Import | Runtime | Purpose |
|
|
17
|
+
| - | - | - |
|
|
18
|
+
| `@digimuza/system-core/config` | Server only | `SystemConfig.load()` / `register(schema)`, Infisical source. Browser builds resolve to a stub that throws. |
|
|
19
|
+
| `@digimuza/system-core/modules` | Any | `defineModule()`, manifest types, `PermissionOf<>`. |
|
|
20
|
+
| `@digimuza/system-core/convex` | Convex | `systemTables`, `defineConvexSystem()`, `requirePermission`, all core services. |
|
|
21
|
+
| `@digimuza/system-core/next` | Next.js server | `createSystemServerClient()` (session token, tracing, typed errors), `createAgentClient()`. |
|
|
22
|
+
| `@digimuza/system-core/react` | Client components | `<SystemProvider>`, `useCan`, `useVisibleModules`, `useCurrentUser`, `useFeatureFlag`, `<Can>`. |
|
|
23
|
+
| `@digimuza/system-core` | Any | `SystemError`, redaction, logger, tracer, shared types. |
|
|
24
|
+
| `@digimuza/system-core/testing` | Tests | `MemoryStore` (same semantics as Convex, serializable transactions). |
|
|
25
|
+
|
|
26
|
+
These seven subpaths, listed in `package.json#exports`, are the whole public surface. Deep imports
|
|
27
|
+
(`@digimuza/system-core/dist/...`) are not supported. Server and browser entries stay separate:
|
|
28
|
+
`.`, `./modules` and `./react` never import `./config`, `./convex` or `./next`, and never read
|
|
29
|
+
`process.env`. `npm run check:pack` enforces this in CI. Removing or renaming an entry or export,
|
|
30
|
+
or moving code across the server/browser line, is a breaking change (a major bump; while on 0.x,
|
|
31
|
+
a minor bump with a changelog note).
|
|
32
|
+
|
|
33
|
+
## 1. Declare business modules
|
|
34
|
+
|
|
35
|
+
```ts
|
|
36
|
+
// modules/finance.ts
|
|
37
|
+
import { defineModule } from "@digimuza/system-core/modules"
|
|
38
|
+
|
|
39
|
+
export const finance = defineModule({
|
|
40
|
+
id: "finance",
|
|
41
|
+
kind: "business",
|
|
42
|
+
description: "Sąskaitų peržiūra ir tvirtinimas",
|
|
43
|
+
permissions: [
|
|
44
|
+
{ id: "finance:invoice:read", name: "Skaityti", description: "Peržiūrėti sąskaitą" },
|
|
45
|
+
{ id: "finance:invoice:approve", name: "Tvirtinti", description: "Patvirtinti sąskaitą" },
|
|
46
|
+
],
|
|
47
|
+
initialSubroles: [
|
|
48
|
+
{ id: "finance:reader", name: "Skaitytojas", description: "Gali peržiūrėti sąskaitas", permissions: ["finance:invoice:read"] },
|
|
49
|
+
],
|
|
50
|
+
featureFlags: [{ id: "finance:bulk-approve", description: "Bulk approval", default: false }],
|
|
51
|
+
})
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
`defineModule` validates when it is imported: ID formats, permission prefixes,
|
|
55
|
+
and subroles that reference only their own module's permissions. When the
|
|
56
|
+
modules are composed, ID collisions and unknown dependencies are rejected.
|
|
57
|
+
|
|
58
|
+
## 2. Convex
|
|
59
|
+
|
|
60
|
+
```ts
|
|
61
|
+
// convex/schema.ts
|
|
62
|
+
import { defineSchema, defineTable } from "convex/server"
|
|
63
|
+
import { systemTables } from "@digimuza/system-core/convex"
|
|
64
|
+
export default defineSchema({ ...systemTables, invoices: defineTable({ /* … */ }) })
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
```ts
|
|
68
|
+
// convex/system.ts (the module path must match `functionsModule`, default "system")
|
|
69
|
+
import { defineConvexSystem } from "@digimuza/system-core/convex"
|
|
70
|
+
import { finance } from "../modules/finance"
|
|
71
|
+
|
|
72
|
+
export const system = defineConvexSystem({
|
|
73
|
+
modules: [finance],
|
|
74
|
+
// superUserEmails defaults to process.env.SUPER_USER_EMAILS (set on the Convex deployment)
|
|
75
|
+
inviteDelivery: { deliver: async ({ email, url, operationId }) => { /* send email */ } },
|
|
76
|
+
inviteUrl: (token) => `${process.env.APP_URL}/invite?token=${token}`,
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
export const {
|
|
80
|
+
session, completeSignIn, registerSystem,
|
|
81
|
+
listModules, setModuleEnabled, listPermissions, explainDecision,
|
|
82
|
+
getUser, findUserByIdentity, listUsers, suspendUser, activateUser, getMyProfile, updateMyProfile,
|
|
83
|
+
listRoles, getRole, listSubroles, createRole, copyRole, updateRole, archiveRole,
|
|
84
|
+
createSubrole, copySubrole, previewSubroleChange, updateSubrole, archiveSubrole,
|
|
85
|
+
assignRoles, revokeRole, setDelegationScope,
|
|
86
|
+
listInvites, getInvite, revokeInvite, createInvite, resendInvite, _createInvite, _resendInvite, _recordInviteDelivery,
|
|
87
|
+
listMyApiKeys, setApiKeyRoles, revokeApiKey, createApiKey, rotateApiKey, _createApiKey, _rotateApiKey,
|
|
88
|
+
listFeatureFlags, setFeatureFlag, resetFeatureFlag, listAudit,
|
|
89
|
+
} = system.functions
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Business functions guard themselves on the server, in the same transaction as
|
|
93
|
+
the change:
|
|
94
|
+
|
|
95
|
+
```ts
|
|
96
|
+
// convex/invoices.ts
|
|
97
|
+
import { mutation } from "./_generated/server"
|
|
98
|
+
import { v } from "convex/values"
|
|
99
|
+
import { recordAudit } from "@digimuza/system-core/convex"
|
|
100
|
+
import { system } from "./system"
|
|
101
|
+
|
|
102
|
+
export const approve = mutation({
|
|
103
|
+
args: { invoiceId: v.id("invoices"), apiKey: v.optional(v.string()) },
|
|
104
|
+
handler: async (ctx, args) => {
|
|
105
|
+
const { sys, actor } = await system.requirePermission(ctx, "finance:invoice:approve", { apiKey: args.apiKey })
|
|
106
|
+
// Object-level checks (ownership, allowed state transition) go here.
|
|
107
|
+
await ctx.db.patch("invoices", args.invoiceId, { approved: true })
|
|
108
|
+
await recordAudit(sys, actor, { action: "finance.invoice.approve", targetType: "invoice", targetId: args.invoiceId })
|
|
109
|
+
return { operationId: sys.operationId }
|
|
110
|
+
},
|
|
111
|
+
})
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
Background jobs store `toActorRef(actor)` and call `authorizeJob(sys, ref, permission)`
|
|
115
|
+
when they run. A `null` result means the job must do nothing. External side
|
|
116
|
+
effects use `claimOperation` and `completeOperation` for replay protection.
|
|
117
|
+
|
|
118
|
+
## 3. Deployment (Coolify)
|
|
119
|
+
|
|
120
|
+
1. Pin the app version and the package version.
|
|
121
|
+
2. Coolify provides `INFISCAL_BASE_URL`, `INFISCAL_CLIENT_ID`, `INFISCAL_SECRET`,
|
|
122
|
+
`INFISCAL_PATH`, `INFISCAL_ENV`, `INFISCAL_PROJECT_ID` and `SUPER_USER_EMAILS`.
|
|
123
|
+
3. `npx convex deploy`, then `npx convex run system:registerSystem`. Running it
|
|
124
|
+
again is safe. Conflicts abort the whole registration, so a partial catalog
|
|
125
|
+
is never activated.
|
|
126
|
+
4. On the first sign-in of a verified email from `SUPER_USER_EMAILS`, the
|
|
127
|
+
Super account is created. Everyone else needs an invite.
|
|
128
|
+
|
|
129
|
+
## 4. Next.js server
|
|
130
|
+
|
|
131
|
+
```ts
|
|
132
|
+
import SystemConfig from "@digimuza/system-core/config"
|
|
133
|
+
import { createSystemServerClient } from "@digimuza/system-core/next"
|
|
134
|
+
import { z } from "zod"
|
|
135
|
+
|
|
136
|
+
await SystemConfig.load()
|
|
137
|
+
const env = SystemConfig.register(z.object({ APP_URL: z.string().url() }), { moduleId: "web" })
|
|
138
|
+
|
|
139
|
+
const client = createSystemServerClient({ getToken: () => getConvexToken(), headers: await headers() })
|
|
140
|
+
const me = await client.query(api.system.session, { operationId: client.operationId })
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
Agents call system-aware functions with `createAgentClient({ url, apiKey })`.
|
|
144
|
+
|
|
145
|
+
## 5. Client components
|
|
146
|
+
|
|
147
|
+
```tsx
|
|
148
|
+
"use client"
|
|
149
|
+
import { SystemProvider, Can, useVisibleModules } from "@digimuza/system-core/react"
|
|
150
|
+
|
|
151
|
+
<SystemProvider session={api.system.session}>
|
|
152
|
+
<Can permission="finance:invoice:approve"><ApproveButton /></Can>
|
|
153
|
+
</SystemProvider>
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
These values only decide what the UI shows. Convex re-checks every call. The
|
|
157
|
+
subscription is live, so the UI updates when a permission is revoked.
|
|
158
|
+
|
|
159
|
+
## Errors
|
|
160
|
+
|
|
161
|
+
Public functions throw `ConvexError({ code, message, operationId, fields? })`
|
|
162
|
+
with `code` ∈ `UNAUTHENTICATED | FORBIDDEN | NOT_FOUND | VALIDATION | CONFLICT | UNAVAILABLE`.
|
|
163
|
+
`toSystemError(e)` normalizes the error on the client. The exact denial reason
|
|
164
|
+
is only logged and is available through `explainDecision` (diagnostics,
|
|
165
|
+
`permissions:decision:explain`). Records the actor may not see return
|
|
166
|
+
`NOT_FOUND`, the same as records that don't exist.
|
|
167
|
+
|
|
168
|
+
## Development
|
|
169
|
+
|
|
170
|
+
```bash
|
|
171
|
+
npm test # vitest: acceptance scenarios, Convex end-to-end (convex-test), units
|
|
172
|
+
npm run typecheck # TypeScript 7
|
|
173
|
+
npm run build # tsc → dist/
|
|
174
|
+
npm run check:pack # pack, install into a clean consumer, import + type-check every entry, browser/server separation
|
|
175
|
+
npm run lint # biome lint
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
## Releasing
|
|
179
|
+
|
|
180
|
+
Releases are cut by [Changesets](https://github.com/changesets/changesets) and published from
|
|
181
|
+
GitHub Actions (`.github/workflows/release.yml`) with npm trusted publishing (OIDC, no long-lived token).
|
|
182
|
+
|
|
183
|
+
1. In each PR that changes the package, run `npx changeset`, choose patch/minor/major and describe the change.
|
|
184
|
+
2. On merge to `main`, the Release workflow opens or updates a **"chore: version packages"** PR
|
|
185
|
+
(it bumps `package.json` and writes `CHANGELOG.md`).
|
|
186
|
+
3. Merging that PR publishes the new version to npm, tags `vX.Y.Z` and creates a GitHub release.
|
|
187
|
+
|
|
188
|
+
Publishing is gated by the repository variable `NPM_PUBLISH_ENABLED=true`. Without it, and on manual
|
|
189
|
+
runs with `dry_run`, the workflow only runs `npm publish --dry-run`. CI (`.github/workflows/ci.yml`)
|
|
190
|
+
runs typecheck, lint, test, build and `check:pack` on Node 22, 24 and 26 for every PR and push to `main`.
|
|
191
|
+
|
package/README.md
CHANGED
|
@@ -1,3 +1,157 @@
|
|
|
1
|
-
|
|
1
|
+
Vienai įmonei diegiama atskira Next.js ir Convex aplikacija. `@digimuza/system-core` yra bendras NPM paketas. Kliento aplikacija registruoja verslo modulius, o branduolys tvarko konfigūraciją, tapatybes, teises ir diagnostiką. `finance` ir `powerbi` yra modulio sutarties pavyzdžiai.
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
# Moduliai ir jų paskirtis
|
|
4
|
+
|
|
5
|
+
`config` įkelia ir pagal Zod schemas validuoja serverio konfigūraciją. `auth` susieja patikimo tiekėjo tapatybę su lokaliu vartotoju. `users` saugo stabilų ID, tapatybės sąsają, patvirtinto el. pašto būseną ir paskyros būseną. Kliento papildomi įrašai siejami per `userId`. `profile` leidžia aktyviam vartotojui tvarkyti savo profilio pateikimo laukus. Tapatybės, el. pašto patvirtinimo, būsenos ir rolių keitimas turi atskirus procesus.
|
|
6
|
+
|
|
7
|
+
`roles` valdo roles, subroles, jų sudėtį ir priskyrimus. `permissions` registruoja kode aprašytus veiksmus ir priima prieigos sprendimus. `invites` valdo vienkartinius, atšaukiamus kvietimus su galiojimo terminu ir numatomomis rolėmis. `api-keys` išduoda, rotuoja ir atšaukia agentų raktus. Modulių registras saugo modulių tipą, verslo modulių būseną ir registracijos versiją.
|
|
8
|
+
|
|
9
|
+
`logging` registruoja struktūruotus įvykius. `tracing` sieja Next.js, Convex ir išorinių operacijų žingsnius pagal operacijos ID. `feature-flags` valdo funkcijų prieinamumą jau įjungtuose moduliuose. Funkcijos žyma negali suteikti teisės. Audito įrašai fiksuoja saugumui svarbių pakeitimų autorių, taikinį, laiką, rezultatą ir pakeistas sąsajas. Diagnostikoje maskuojamos paslaptys, sesijų duomenys ir raktai, taip pat ir `Super` rodinyje.
|
|
10
|
+
|
|
11
|
+
# Viešasis paketas ir jo naudotojai
|
|
12
|
+
|
|
13
|
+
Pirmoji versija platinama vienu versijuojamu NPM paketu su atskirais importo keliais. Kiekvienas klientas savo aplikacijoje fiksuoja paketo versiją ir turi atskirą Convex diegimą. Aplikacija pasirenka įtraukiamus verslo modulius, jų ekranus, prisijungimo ir kvietimų pristatymo tiekėjus. Paketas apibrėžia bendras serverio operacijas, duomenų schemas, TypeScript tipus ir sąsajas šioms integracijoms.
|
|
14
|
+
|
|
15
|
+
| Kas integruoja | Siūlomas importo kelias | Ką gauna |
|
|
16
|
+
| - | - | - |
|
|
17
|
+
| Serverio paleidimas | `@digimuza/system-core/config` | `SystemConfig.load()` ir sinchroninį `register(schema)` po įkėlimo |
|
|
18
|
+
| Modulio autorius | `@digimuza/system-core/modules` | `defineModule()`, manifesto tipus ir validaciją |
|
|
19
|
+
| Convex aplikacija | `@digimuza/system-core/convex` | Sistemos registraciją, užklausų ir mutacijų pagalbines funkcijas, teisių patikrą |
|
|
20
|
+
| Next.js serveris | `@digimuza/system-core/next` | Autentifikuotos sesijos perdavimą ir tipizuotą kreipimąsi į Convex |
|
|
21
|
+
| Next.js kliento komponentai | `@digimuza/system-core/react` | Dabartinio vartotojo, matomų modulių ir leidžiamų veiksmų duomenis kliento kuriamam UI |
|
|
22
|
+
|
|
23
|
+
Šie pavadinimai yra projektuojama v1 sutartis. Paleidimo konfigūracija, Infisical klientas ir paslaptys pasiekiami tik serverio importuose. Kliento komponentų importavimas neturi reikalauti serverio aplinkos kintamųjų.
|
|
24
|
+
|
|
25
|
+
# Funkcijų katalogas
|
|
26
|
+
|
|
27
|
+
| Modulis | V1 operacijos | Tikrinamas rezultatas |
|
|
28
|
+
| - | - | - |
|
|
29
|
+
| `config` | Įkelti vieną konfigūracijos versiją, validuoti kelių modulių schemas, pateikti nuasmenintą būseną | Lygiagretūs `load()` naudoja tą patį įkėlimą. `register()` iki įkėlimo grąžina aiškią klaidą. Būsena nerodo reikšmių. |
|
|
30
|
+
| `modules` | Registruoti manifestus, išvardyti įdiegtus modulius, gauti efektyvią būseną, įjungti ar išjungti verslo modulį | Pakartotinis registravimas išsaugo kliento pasirinkimą. Sisteminio modulio išjungimo užklausa atmetama. |
|
|
31
|
+
| `auth` | Pradėti ir užbaigti prisijungimą per pasirinktą tiekėją, nustatyti veikėją, atsijungti | Pasibaigusi sesija neleidžia atlikti apsaugoto veiksmo. Neatpažintai paskyrai reikalingas galiojantis kvietimas arba pradinis patvirtintas `Super` prisijungimas. |
|
|
32
|
+
| `users` | Peržiūrėti vartotoją, rasti pagal tapatybę, puslapiuoti sąrašą, sustabdyti ir aktyvuoti paskyrą | Pakartotinai gavus prisijungimo įvykį lieka vienas vartotojas. Aktyvavimas neatkuria atšauktų raktų. |
|
|
33
|
+
| `profile` | Skaityti ir redaguoti savo rodomą vardą, kalbą ir laiko juostą | Įrašomi tik leidžiami laukai. Kliento profilio plėtiniai turi atskirą schemą ir prieigos taisykles. |
|
|
34
|
+
| `invites` | Sukurti, pristatyti, pakartotinai išsiųsti, atšaukti, priimti, rodyti būseną | Pakartotinis siuntimas pakeičia ankstesnį žetoną. Nepavykus pristatyti galima kartoti nesukuriant naujos paskyros. |
|
|
35
|
+
| `permissions` | Registruoti katalogą, išvardyti teises pagal modulį, patikrinti veiksmą, paaiškinti sprendimą įgaliotam operatoriui | UI ir serveris naudoja tą patį sprendimo modelį. Serveris apsaugo veiksmą net tiesiogiai iškvietus Convex funkciją. |
|
|
36
|
+
| `roles` | Kurti, kopijuoti, redaguoti ir archyvuoti roles bei subroles, peržiūrėti ryšius, priskirti roles ir jas atšaukti | Prieš keitimą galima matyti paveiktas roles ir gavėjų skaičių. Archyvuotas objektas nebesuteikia teisių. |
|
|
37
|
+
| `api-keys` | Sukurti savininko raktą su rolėmis, išvardyti jo raktus, keisti roles, rotuoti ir atšaukti | Raktų sąraše matomas identifikavimo prefiksas, galiojimas ir būsena. Paslaptis parodoma vieną kartą. |
|
|
38
|
+
| `feature-flags` | Registruoti modulio žymas kode, skaityti efektyvią reikšmę, keisti instaliacijos reikšmę, grąžinti numatytąją | V1 žymos yra loginės ir galioja vienai instaliacijai. Nežinoma žyma nesuaktyvina funkcijos. Žymos reikšmė naudojama kartu su modulio ir teisės patikromis. |
|
|
39
|
+
| `logging` | Rašyti struktūruotus įvykius su lygiu, modulio ID, įvykio vardu ir koreliacijos ID | Vienos užklausos klaidą galima rasti pagal ID. Tokenai ir slapti laukai pašalinami prieš siunčiant į žurnalų saugyklą. |
|
|
40
|
+
| `tracing` | Pradėti operaciją, susieti jos žingsnius, perduoti kontekstą foninei užduočiai, užbaigti su rezultatu | Pagal vieną trace ID matomi Next.js, Convex ir integracijos žingsniai, jų trukmės ir klaidos vieta. |
|
|
41
|
+
| `audit` | Įrašyti prieigos bei administravimo pakeitimus, leisti įgaliotam asmeniui filtruoti pagal laiką, veikėją ir objektą | Agentui išsaugomi rakto ir savininko ID. Sėkmingo saugomo pakeitimo auditas įrašomas kartu su DB pakeitimu. |
|
|
42
|
+
|
|
43
|
+
# Naujo modulio sutartis
|
|
44
|
+
|
|
45
|
+
Modulio autorius pateikia manifestą ir registruoja jį aplikacijos sudėtyje. Identifikatorius nekinta tarp versijų. Kiekviena teisė ir pradinė subrolė turi identifikatorių, pavadinimą ir aprašą. Žemiau pateiktas sutrumpintas siūlomo API pavyzdys.
|
|
46
|
+
|
|
47
|
+
```ts
|
|
48
|
+
import { defineModule } from "@digimuza/system-core/modules"
|
|
49
|
+
|
|
50
|
+
export const finance = defineModule({
|
|
51
|
+
id: "finance",
|
|
52
|
+
kind: "business",
|
|
53
|
+
description: "Sąskaitų peržiūra ir tvirtinimas",
|
|
54
|
+
permissions: [
|
|
55
|
+
{ id: "finance:invoice:read", name: "Skaityti", description: "Peržiūrėti sąskaitą" },
|
|
56
|
+
{ id: "finance:invoice:approve", name: "Tvirtinti", description: "Patvirtinti sąskaitą" },
|
|
57
|
+
],
|
|
58
|
+
initialSubroles: [{
|
|
59
|
+
id: "finance:reader",
|
|
60
|
+
name: "Skaitytojas",
|
|
61
|
+
description: "Gali peržiūrėti sąskaitas",
|
|
62
|
+
permissions: ["finance:invoice:read"],
|
|
63
|
+
}],
|
|
64
|
+
})
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Kiekvieno modulio dokumentacijoje privalomi paskirtis, tipas, viešos operacijos, duomenų savininkas, konfigūracijos raktai, teisės, pradinės subrolės, žymos ir bent vienas sėkmės bei atsisakymo scenarijus. Programinė priklausomybė nuo kito modulio nesuteikia jo teisių. Išimtas iš aplikacijos ar nepažįstamas modulis tampa neprieinamas net turint ankstesnį DB priskyrimą.
|
|
68
|
+
|
|
69
|
+
# Administravimo ir integracijų paviršius
|
|
70
|
+
|
|
71
|
+
Kliento dashboardas gali pateikti šiuos rodinius: mano profilis ir raktai, vartotojai ir kvietimai, rolės bei subrolės, modulių katalogas, funkcijų žymos, auditas ir diagnostika. Sąrašai palaiko puslapiavimą. Rolės detalė rodo subroles, jų teises ir gavėjus pagal žiūrinčiojo įgaliojimus. Modulio kortelėje rodoma konfigūruota ir konkrečiam veikėjui efektyvi būsena, kad `Super` aiškiai matytų, ką gali naudoti klientas.
|
|
72
|
+
|
|
73
|
+
Autentifikacijos adapteris grąžina patikrintą tiekėjo tapatybę. Kvietimų siuntimo adapteris gauna gavėją, vienkartinę nuorodą ir pristatymo operacijos ID. Logų ir trace eksportuotojai gauna jau išvalytus įvykius. Adapterių klaidos turi aiškų rezultatą: privalomos tapatybės patikros gedimas blokuoja prieigą, laiško siuntimo gedimas palieka kartojamą pristatymą, diagnostikos eksportuotojo gedimas nekeičia jau įvykdytos verslo operacijos rezultato.
|
|
74
|
+
|
|
75
|
+
Viešos funkcijos pateikia stabilius klaidų kodus autentifikacijos, teisių, duomenų validacijos, konflikto ir laikino nepasiekiamumo atvejams. Klientui grąžinamas operacijos ID. Kai pats įrašo egzistavimas yra ribota informacija, svetimas ir neegzistuojantis įrašas išorei atrodo vienodai. Tiksli atmetimo priežastis pasiekiama tik įgaliotame diagnostikos rodinyje.
|
|
76
|
+
|
|
77
|
+
# Diegimo ir leidimų priėmimas
|
|
78
|
+
|
|
79
|
+
Coolify diegimas turi fiksuoti aplikacijos bei paketo versijas, validuoti konfigūraciją, vykdyti suderinamas Convex migracijas, registruoti manifestus ir patikrinti abiejų vykdymo aplinkų parengtį. Projekto identifikacijai numatomas diegimo parametras `INFISCAL_PROJECT_ID`. Pakartotinas migracijos ar registracijos paleidimas išsaugo rezultatą. Nepavykęs diegimas neaktyvuoja iš dalies užregistruoto katalogo. Grįžimas prie ankstesnės kodo versijos galimas tik jei ji suderinama su aktyvia schema ir teisių katalogu.
|
|
80
|
+
|
|
81
|
+
Prieš publikuojant versiją etaloninė Next.js + Convex aplikacija įdiegia supakuotą NPM artefaktą. Tikrinami deklaruoti importai ir tipai, serverio ir naršyklės paketų atskyrimas, įdiegimas į tuščią Convex, atnaujinimas iš ankstesnės palaikomos versijos ir to paties diegimo pakartojimas. Suderinamos Next.js, React, Convex ir Zod versijos nurodomos leidimo peer dependencies ir tikrinamos CI.
|
|
82
|
+
|
|
83
|
+
#
|
|
84
|
+
|
|
85
|
+
Priėmimo bandyme operatorius pakviečia darbuotoją, darbuotojas prisijungia ir susikuria ribotos rolės raktą. Įjungus bandomąjį verslo modulį agentas atlieka leidžiamą veiksmą. Tada pakeičiama rolė, atšaukiamas raktas ir tikrinama, ar jau suplanuotas darbas sustabdomas. Pagal tą pačią operaciją turi būti galima susieti audito įrašą ir techninę diagnostiką. Papildomai kartojamas nepavykęs kvietimo pristatymas, simuliuojamas išjungtas diagnostikos eksportuotojas ir patikrinama, kad jokioje naršyklės ar žurnalo išvestyje nėra paslapčių.
|
|
86
|
+
|
|
87
|
+
# Konfigūracija ir diegimas
|
|
88
|
+
|
|
89
|
+
`import SystemConfig from "@digimuza/system-core/config"` → `await SystemConfig.load()` → `const env = SystemConfig.register(schema)`. Coolify pateikia pradinius `INFISCAL_BASE_URL`, `INFISCAL_CLIENT_ID`, `INFISCAL_SECRET`, `INFISCAL_PATH` ir `INFISCAL_ENV`. Diegimo konfigūracija taip pat turi vienareikšmiškai identifikuoti Infisical projektą. Aplikacijos paslapčių šaltinis yra Infisical. Trūkstant privalomos reikšmės atitinkamas servisas nepasiekia parengties būsenos. Klaidoje pateikiamas rakto pavadinimas.
|
|
90
|
+
|
|
91
|
+
Next.js ir Convex vykdymo aplinkoms konfigūracija pateikiama atskirai. Convex užklausos ir mutacijos prieigos patikrą atlieka naudodamos lokaliai prieinamą aktyvios versijos konfigūraciją. Diegimas laikomas baigtu, kai ankstesnės versijos instancijos nebeaptarnauja užklausų. Naršyklė gauna tik aiškiai viešomis paskelbtas reikšmes.
|
|
92
|
+
|
|
93
|
+
# Rolės ir modulio registracija
|
|
94
|
+
|
|
95
|
+
Vienintelė įmonės valdomų suteikimų grandinė yra gavėjas → rolė → subrolė → teisė. Gavėjas yra vartotojas arba agento API raktas. Kiekviename lygyje leidžiama turėti kelias sąsajas. Viena subrolė priklauso vienam moduliui. Rolė gali sujungti kelių modulių subroles. Tiesioginis subrolės ar teisės suteikimas gavėjui yra neleistina operacija.
|
|
96
|
+
|
|
97
|
+
Teisės raktas yra stabilus `modulis:objektas:veiksmas`. Kiekvienas modulis kode deklaruoja savo ID, tipą, paskirtį, teises ir pradinių subrolių šablonus. Diegimo registracija pagal stabilius ID sukuria trūkstamus įrašus. Esamų subrolių administratoriaus pakeitimai ir modulio būsena išsaugomi. Nauja teisė atsiranda kataloge. Jos įtraukimas į jau naudojamą subrolę turi būti sąmoningas DB pakeitimas. Pasenusi arba nežinoma teisė atmetama, identifikatoriai naujai prasmei nenaudojami. Nesuderinamos registracijos ir ID kolizijos sustabdo diegimą.
|
|
98
|
+
|
|
99
|
+
Sisteminiai moduliai visada įjungti. Verslo modulis pirmą kartą registruojamas išjungtas. Jį įjungti ar išjungti gali tik `Super`. Išjungus išlieka duomenys ir priskyrimai. `Admin` yra įprasta administravimo rolė su konkrečiomis teisėmis. `Super` nustatomas pagal aktyvios paskyros patikimo autentifikacijos tiekėjo patvirtintą el. paštą ir diegimo `SUPER_USER_EMAILS` sąrašą. Sąrašas taikomas po pakartotinio diegimo. DB rolės pavadinimas šio statuso nesuteikia.
|
|
100
|
+
|
|
101
|
+
# Prieigos sprendimas
|
|
102
|
+
|
|
103
|
+
Serveris nustato veikėją iš sesijos arba API rakto. Neaiški tapatybė, nežinoma teisė, neveikianti paskyra ar prieigos patikros klaida užklausą atmeta. Autentifikacijos pradžia ir kvietimo priėmimas turi atskirus aiškiai aprašytus įėjimo taškus. Verslo kode kviečiama `await requirePermission(ctx, "finance:invoice:approve")`. Konkretų objektą turinčioms teisėms serverio funkcija papildomai tikrina objekto ryšį ir leistiną būsenos perėjimą.
|
|
104
|
+
|
|
105
|
+
Žmogui teisė galioja, kai bent viena jo rolė ją suteikia ir modulis įjungtas. Aktyviam vartotojui automatiškai priskiriama apsaugota profilio rolė, suteikianti visas savo profilio teises. Profilio savininką serveris nustato iš sesijos. Aktyvus `Super` gali naudoti visus registruotus modulius ir visas teises, įskaitant diagnostiką. Verslo duomenų vientisumo taisyklės galioja visiems.
|
|
106
|
+
|
|
107
|
+
Agentui būtinas aktyvus raktas, aktyvus savininkas, įjungtas modulis, teisė per rakto roles ir ta pati šiuo metu galiojanti savininko teisė. Tikrinama teisės, o ne rolės ID sankirta. Kiekvieno rakto rolės nepriklausomos. `Super` statusas, išjungtų modulių išimtis ir asmeninės sesijos veiksmai raktui nepersiduoda. Pirmajai versijai raktų išdavimas, prieigos administravimas ir `Super` diagnostika skirti tik žmogaus sesijai.
|
|
108
|
+
|
|
109
|
+
# Delegavimas ir paskyrų gyvavimo ciklas
|
|
110
|
+
|
|
111
|
+
Rolės priskyrimui reikia ir `roles:assignment:create`, ir leidimo suteikti konkrečią tikslinę rolę. Delegavimo apimtis saugoma pagal stabilius rolių ID. Rolę galima suteikti tik jei visos jos suteikiamos teisės telpa į suteikiančiojo įgaliojimus. Vertinamos ir išjungtų modulių teisės, kad modulis vėliau neaktyvuotų neautorizuotų priskyrimų. Įprastas delegatas negali keisti savo įgaliojimų, saugomų rolių ar delegavimo ribų. Pradinę `Admin` rolę įmonei suteikia `Super`.
|
|
112
|
+
|
|
113
|
+
Subrolės redagavimas keičia visas ją naudojančias roles. Prieš pakeitimą serveris įvertina paveiktas roles ir gavėjus. Pakeitimas leidžiamas tik valdant visas paveiktas roles ir galint deleguoti pridedamas teises. Jei bendrą subrolę naudoja saugoma rolė, administratorius gali kurti atskirą kopiją savo valdomai rolei. Šios taisyklės galioja ir importui, masiniams veiksmams bei kvietimams.
|
|
114
|
+
|
|
115
|
+
Kvietimas susiejamas su el. paštu ir konkrečių rolių ID. Priėmimo metu tikrinamas patvirtintas gavėjo adresas, galiojimas, vienkartinis panaudojimas ir dabartinė kvietėjo teisė suteikti kiekvieną rolę. Pasikeitus įgaliojimams visas priėmimas atmetamas. Paskyra, priskyrimai ir kvietimo panaudojimas įrašomi atominiu veiksmu. Prisijungimo tapatybė susiejama pagal patikimo tiekėjo stabilų subjekto ID. Vien sutampančio el. pašto nepakanka sulieti paskyras.
|
|
116
|
+
|
|
117
|
+
API rakto paslaptis rodoma vieną kartą. Convex saugo rakto ID, kriptografinę patikros reikšmę, savininką, pavadinimą, galiojimą ir atšaukimo būseną. Panaikinus raktą jo atkurti negalima. Blokavus arba uždarius savininko paskyrą atšaukiami jo raktai. Savininkui praradus tik vieną teisę kitos rakto teisės išlieka. Savininkui vėl gavus teisę ji vėl veikia, jei raktas tebėra aktyvus ir jo rolė vis dar ją suteikia.
|
|
118
|
+
|
|
119
|
+
# Convex įgyvendinimo sutartis
|
|
120
|
+
|
|
121
|
+
Ryšiams numatytos atskiros indeksuotos lentelės: `userRoles`, `apiKeyRoles`, `roleSubroles`, `subrolePermissions`. Atskirai saugomi `users`, `roles`, `subroles`, `modules`, `apiKeys`, `invites`, delegavimo ribos ir audito įvykiai. Užtikrinamas porų unikalumas, pakartotinių operacijų saugumas ir puslapiavimas. Nėra produkto nustatyto rolių skaičiaus limito. Didžiausiai palaikomai instaliacijai prieš išleidimą atliekamas apkrovos bandymas. Jei prieigos sprendimo nepavyksta apskaičiuoti iki galo, jis atmetamas.
|
|
122
|
+
|
|
123
|
+
Next.js UI gali paslėpti veiksmą, tačiau visos viešos Convex funkcijos tikrina veikėją serveryje. Mutacija tikrina teises ir keičia duomenis vienoje transakcijoje. Užklausa skaito aktualius priskyrimus. Ilgalaikis JWT arba naršyklės kešas negali išsaugoti atšauktos teisės. Po atšaukimo patikra taikoma ir aktyvioms prenumeratoms. Jau vartotojui perduotų duomenų atšaukimas nesugrąžina.
|
|
124
|
+
|
|
125
|
+
Foninė užduotis saugo veikėjo ir rakto ID, vykdymo metu iš naujo tikrina būseną ir teises. Išorinis veiksmas vykdomas pagal identifikuojamą operaciją su apsauga nuo pakartojimo. Atšaukimas sustabdo dar nepradėtą darbą. Jei išorinė sistema jau priėmė komandą, rezultatas užfiksuojamas audite. Vidiniams sistemos priežiūros darbams naudojamas siauras atskiras įėjimo taškas be vartotojo `Super` privilegijų.
|
|
126
|
+
|
|
127
|
+
# Privalomi priėmimo scenarijai
|
|
128
|
+
|
|
129
|
+
1. Gavėjas gauna tą pačią teisę per dvi roles. Pašalinus vieną kelią veiksmas leidžiamas, pašalinus paskutinį atmetamas.
|
|
130
|
+
|
|
131
|
+
2. Pasikeitus subrolei pokytis pasiekia ir vartotoją, ir rakto roles nuo kitos patikros. Agentui savininko teisės išlieka viršutine riba.
|
|
132
|
+
|
|
133
|
+
3. Raktui tiesiogiai siunčiamas teisės arba subrolės ID. Serveris atmeta visą priskyrimą.
|
|
134
|
+
|
|
135
|
+
4. `Super` savininko raktas be rolių neturi verslo teisių. Su finansų role jis gali naudoti tik įjungtą `finance` modulį.
|
|
136
|
+
|
|
137
|
+
5. Išjungus modulį blokuojami UI, tiesioginė API, agentas ir laukiančios užduotys. Žmogaus `Super` sesijai modulis prieinamas. Įjungus grįžta anksčiau suteiktos teisės.
|
|
138
|
+
|
|
139
|
+
6. Pakeitęs profilio užklausos `userId`, vartotojas negali perskaityti ar redaguoti kito profilio. Asmeninis profilio veiksmas negali pakeisti `Super` identiteto.
|
|
140
|
+
|
|
141
|
+
7. Kvietimas panaudojamas du kartus, pasibaigia arba kvietėjas netenka įgaliojimų. Papildomų priskyrimų nesukuriama.
|
|
142
|
+
|
|
143
|
+
8. Raktas atšaukiamas tarp užduoties sukūrimo ir vykdymo. Užduotis nesukuria šalutinio poveikio.
|
|
144
|
+
|
|
145
|
+
9. Delegatas mėgina suteikti `Admin` per kvietimą, pakeisti bendrą subrolę ar savo delegavimo ribas. Operacija atmetama.
|
|
146
|
+
|
|
147
|
+
10. Du lygiagretūs pakeitimai mėgina apeiti delegavimo ribą. Galutinė būsena tenkina tas pačias taisykles kaip nuosekliai įvykdyti patikrinti pakeitimai.
|
|
148
|
+
|
|
149
|
+
11. Pakartotinė modulio registracija išsaugo vartotojų pakeitimus ir įjungimo būseną. Nauja teisė savaime nepatenka į seną `Full` subrolę.
|
|
150
|
+
|
|
151
|
+
12. Po diegimo iš `SUPER_USER_EMAILS` pašalinta aktyvi sesija praranda `Super`. Nepatvirtintas adresas arba DB rolė vardu `Super` privilegijos nesuteikia.
|
|
152
|
+
|
|
153
|
+
13. Nežinoma teisė, ištrinta rolė, pasibaigęs raktas ar užblokuotas savininkas nesuteikia prieigos. Audite galima atskirti agentą nuo jo savininko.
|
|
154
|
+
|
|
155
|
+
# Techniniai šaltiniai
|
|
156
|
+
|
|
157
|
+
[OWASP Authorization Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Authorization_Cheat_Sheet.html) · [Convex transakcijos](https://docs.convex.dev/database/advanced/occ) · [Convex actions](https://docs.convex.dev/functions/actions)
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
declare const fail: () => never;
|
|
2
|
+
export declare const envSource: typeof fail;
|
|
3
|
+
export declare const staticSource: typeof fail;
|
|
4
|
+
export declare const infisicalSource: typeof fail;
|
|
5
|
+
export declare const createSystemConfig: typeof fail;
|
|
6
|
+
export declare const INFISICAL_BOOTSTRAP_KEYS: readonly string[];
|
|
7
|
+
export declare class ConfigError extends Error {
|
|
8
|
+
}
|
|
9
|
+
declare const SystemConfig: {
|
|
10
|
+
load: typeof fail;
|
|
11
|
+
register: typeof fail;
|
|
12
|
+
status: typeof fail;
|
|
13
|
+
reset: typeof fail;
|
|
14
|
+
};
|
|
15
|
+
export default SystemConfig;
|
|
16
|
+
export { SystemConfig };
|
|
17
|
+
//# sourceMappingURL=browser.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"browser.d.ts","sourceRoot":"","sources":["../../src/config/browser.ts"],"names":[],"mappings":"AAEA,QAAA,MAAM,IAAI,QAAO,KAEhB,CAAA;AAED,eAAO,MAAM,SAAS,aAAO,CAAA;AAC7B,eAAO,MAAM,YAAY,aAAO,CAAA;AAChC,eAAO,MAAM,eAAe,aAAO,CAAA;AACnC,eAAO,MAAM,kBAAkB,aAAO,CAAA;AACtC,eAAO,MAAM,wBAAwB,EAAE,SAAS,MAAM,EAAO,CAAA;AAC7D,qBAAa,WAAY,SAAQ,KAAK;CAAG;AAEzC,QAAA,MAAM,YAAY;IAAK,IAAI;IAAQ,QAAQ;IAAQ,MAAM;IAAQ,KAAK;CAAQ,CAAA;eAC/D,YAAY;AAC3B,OAAO,EAAE,YAAY,EAAE,CAAA"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// Browser stub for `@digimuza/system-core/config`. Configuration, the Infisical
|
|
2
|
+
// client and secrets are server-only; this file keeps them out of client bundles.
|
|
3
|
+
const fail = () => {
|
|
4
|
+
throw new Error("@digimuza/system-core/config is server-only and cannot be used in the browser");
|
|
5
|
+
};
|
|
6
|
+
export const envSource = fail;
|
|
7
|
+
export const staticSource = fail;
|
|
8
|
+
export const infisicalSource = fail;
|
|
9
|
+
export const createSystemConfig = fail;
|
|
10
|
+
export const INFISICAL_BOOTSTRAP_KEYS = [];
|
|
11
|
+
export class ConfigError extends Error {
|
|
12
|
+
}
|
|
13
|
+
const SystemConfig = { load: fail, register: fail, status: fail, reset: fail };
|
|
14
|
+
export default SystemConfig;
|
|
15
|
+
export { SystemConfig };
|
|
16
|
+
//# sourceMappingURL=browser.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"browser.js","sourceRoot":"","sources":["../../src/config/browser.ts"],"names":[],"mappings":"AAAA,gFAAgF;AAChF,kFAAkF;AAClF,MAAM,IAAI,GAAG,GAAU,EAAE;IACvB,MAAM,IAAI,KAAK,CAAC,+EAA+E,CAAC,CAAA;AAClG,CAAC,CAAA;AAED,MAAM,CAAC,MAAM,SAAS,GAAG,IAAI,CAAA;AAC7B,MAAM,CAAC,MAAM,YAAY,GAAG,IAAI,CAAA;AAChC,MAAM,CAAC,MAAM,eAAe,GAAG,IAAI,CAAA;AACnC,MAAM,CAAC,MAAM,kBAAkB,GAAG,IAAI,CAAA;AACtC,MAAM,CAAC,MAAM,wBAAwB,GAAsB,EAAE,CAAA;AAC7D,MAAM,OAAO,WAAY,SAAQ,KAAK;CAAG;AAEzC,MAAM,YAAY,GAAG,EAAE,IAAI,EAAE,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,CAAA;AAC9E,eAAe,YAAY,CAAA;AAC3B,OAAO,EAAE,YAAY,EAAE,CAAA"}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import type { z } from "zod";
|
|
2
|
+
import { SystemError } from "../core/errors.js";
|
|
3
|
+
export type ConfigValues = Readonly<Record<string, string>>;
|
|
4
|
+
/** A source of configuration values (env, Infisical, static for tests). */
|
|
5
|
+
export interface ConfigSource {
|
|
6
|
+
readonly name: string;
|
|
7
|
+
load(): Promise<Record<string, string>>;
|
|
8
|
+
}
|
|
9
|
+
export declare class ConfigError extends SystemError {
|
|
10
|
+
/** Only key names — never values. */
|
|
11
|
+
readonly keys: string[];
|
|
12
|
+
constructor(reason: string, keys: string[], message: string);
|
|
13
|
+
}
|
|
14
|
+
type Env = Record<string, string | undefined>;
|
|
15
|
+
/** Reads process environment variables (Coolify deployment parameters). */
|
|
16
|
+
export declare function envSource(env?: Env): ConfigSource;
|
|
17
|
+
export declare function staticSource(values: Record<string, string>, name?: string): ConfigSource;
|
|
18
|
+
/** Bootstrap parameters Coolify provides for Infisical. */
|
|
19
|
+
export declare const INFISICAL_BOOTSTRAP_KEYS: readonly ["INFISCAL_BASE_URL", "INFISCAL_CLIENT_ID", "INFISCAL_SECRET", "INFISCAL_PATH", "INFISCAL_ENV", "INFISCAL_PROJECT_ID"];
|
|
20
|
+
export interface InfisicalSourceOptions {
|
|
21
|
+
env?: Env;
|
|
22
|
+
fetch?: typeof fetch;
|
|
23
|
+
timeoutMs?: number;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Loads secrets from Infisical using Universal Auth. Bootstrap credentials come
|
|
27
|
+
* from the deployment environment; missing ones produce a ConfigError naming the keys.
|
|
28
|
+
*/
|
|
29
|
+
export declare function infisicalSource(options?: InfisicalSourceOptions): ConfigSource;
|
|
30
|
+
export interface RegisteredSchema {
|
|
31
|
+
moduleId: string;
|
|
32
|
+
keys: string[];
|
|
33
|
+
}
|
|
34
|
+
/** De-identified status: names only, never values. */
|
|
35
|
+
export interface ConfigStatus {
|
|
36
|
+
state: "idle" | "loading" | "loaded" | "failed";
|
|
37
|
+
/** Incremented on each successful load. */
|
|
38
|
+
version: number;
|
|
39
|
+
loadedAt: number | null;
|
|
40
|
+
sources: string[];
|
|
41
|
+
keys: string[];
|
|
42
|
+
registered: RegisteredSchema[];
|
|
43
|
+
error?: {
|
|
44
|
+
reason: string;
|
|
45
|
+
keys: string[];
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
export interface SystemConfigOptions {
|
|
49
|
+
/** Default: env, then Infisical (Infisical wins on conflicts). */
|
|
50
|
+
sources?: readonly ConfigSource[];
|
|
51
|
+
}
|
|
52
|
+
export interface SystemConfigApi {
|
|
53
|
+
/** Loads one configuration version. Concurrent calls share the same load. */
|
|
54
|
+
load(): Promise<ConfigStatus>;
|
|
55
|
+
/**
|
|
56
|
+
* Synchronously validates and returns typed values for `schema`.
|
|
57
|
+
* Throws ConfigError before `load()` finished or when keys are missing/invalid.
|
|
58
|
+
*/
|
|
59
|
+
register<S extends z.ZodType>(schema: S, options?: {
|
|
60
|
+
moduleId?: string;
|
|
61
|
+
}): z.infer<S>;
|
|
62
|
+
status(): ConfigStatus;
|
|
63
|
+
/** Testing only: forget the loaded version. */
|
|
64
|
+
reset(): void;
|
|
65
|
+
}
|
|
66
|
+
export declare function createSystemConfig(options?: SystemConfigOptions): SystemConfigApi;
|
|
67
|
+
/** Process-wide instance: `await SystemConfig.load(); const env = SystemConfig.register(schema)`. */
|
|
68
|
+
declare const SystemConfig: SystemConfigApi;
|
|
69
|
+
export default SystemConfig;
|
|
70
|
+
export { SystemConfig };
|
|
71
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/config/index.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AAC5B,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAA;AAE/C,MAAM,MAAM,YAAY,GAAG,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAA;AAE3D,2EAA2E;AAC3E,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,IAAI,IAAI,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAA;CACxC;AAED,qBAAa,WAAY,SAAQ,WAAW;IAC1C,qCAAqC;IACrC,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,CAAA;IACvB,YAAY,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,EAAE,OAAO,EAAE,MAAM,EAI1D;CACF;AAED,KAAK,GAAG,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAA;AAG7C,2EAA2E;AAC3E,wBAAgB,SAAS,CAAC,GAAG,GAAE,GAAkB,GAAG,YAAY,CAS/D;AAED,wBAAgB,YAAY,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAAE,IAAI,SAAW,GAAG,YAAY,CAE1F;AAED,2DAA2D;AAC3D,eAAO,MAAM,wBAAwB,YACnC,mBAAmB,EACnB,oBAAoB,EACpB,iBAAiB,EACjB,eAAe,EACf,cAAc,EACd,qBAAqB,CACb,CAAA;AAEV,MAAM,WAAW,sBAAsB;IACrC,GAAG,CAAC,EAAE,GAAG,CAAA;IACT,KAAK,CAAC,EAAE,OAAO,KAAK,CAAA;IACpB,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AAED;;;GAGG;AACH,wBAAgB,eAAe,CAAC,OAAO,GAAE,sBAA2B,GAAG,YAAY,CAoClF;AAED,MAAM,WAAW,gBAAgB;IAAG,QAAQ,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,EAAE,CAAA;CAAE;AAEtE,sDAAsD;AACtD,MAAM,WAAW,YAAY;IAC3B,KAAK,EAAE,MAAM,GAAG,SAAS,GAAG,QAAQ,GAAG,QAAQ,CAAA;IAC/C,2CAA2C;IAC3C,OAAO,EAAE,MAAM,CAAA;IACf,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAA;IACvB,OAAO,EAAE,MAAM,EAAE,CAAA;IACjB,IAAI,EAAE,MAAM,EAAE,CAAA;IACd,UAAU,EAAE,gBAAgB,EAAE,CAAA;IAC9B,KAAK,CAAC,EAAE;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,EAAE,CAAA;KAAE,CAAA;CAC3C;AAED,MAAM,WAAW,mBAAmB;IAClC,kEAAkE;IAClE,OAAO,CAAC,EAAE,SAAS,YAAY,EAAE,CAAA;CAClC;AAED,MAAM,WAAW,eAAe;IAC9B,6EAA6E;IAC7E,IAAI,IAAI,OAAO,CAAC,YAAY,CAAC,CAAA;IAC7B;;;OAGG;IACH,QAAQ,CAAC,CAAC,SAAS,CAAC,CAAC,OAAO,EAAE,MAAM,EAAE,CAAC,EAAE,OAAO,CAAC,EAAE;QAAE,QAAQ,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAA;IACrF,MAAM,IAAI,YAAY,CAAA;IACtB,+CAA+C;IAC/C,KAAK,IAAI,IAAI,CAAA;CACd;AAOD,wBAAgB,kBAAkB,CAAC,OAAO,GAAE,mBAAwB,GAAG,eAAe,CAyErF;AAED,qGAAqG;AACrG,QAAA,MAAM,YAAY,EAAE,eAAsC,CAAA;eAC3C,YAAY;AAC3B,OAAO,EAAE,YAAY,EAAE,CAAA"}
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import { SystemError } from "../core/errors.js";
|
|
2
|
+
export class ConfigError extends SystemError {
|
|
3
|
+
/** Only key names — never values. */
|
|
4
|
+
keys;
|
|
5
|
+
constructor(reason, keys, message) {
|
|
6
|
+
super("UNAVAILABLE", message, { reason, fields: keys });
|
|
7
|
+
this.name = "ConfigError";
|
|
8
|
+
this.keys = keys;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
const processEnv = () => (typeof process !== "undefined" && process.env ? process.env : {});
|
|
12
|
+
/** Reads process environment variables (Coolify deployment parameters). */
|
|
13
|
+
export function envSource(env = processEnv()) {
|
|
14
|
+
return {
|
|
15
|
+
name: "env",
|
|
16
|
+
async load() {
|
|
17
|
+
const out = {};
|
|
18
|
+
for (const [k, v] of Object.entries(env))
|
|
19
|
+
if (typeof v === "string")
|
|
20
|
+
out[k] = v;
|
|
21
|
+
return out;
|
|
22
|
+
},
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
export function staticSource(values, name = "static") {
|
|
26
|
+
return { name, load: async () => ({ ...values }) };
|
|
27
|
+
}
|
|
28
|
+
/** Bootstrap parameters Coolify provides for Infisical. */
|
|
29
|
+
export const INFISICAL_BOOTSTRAP_KEYS = [
|
|
30
|
+
"INFISCAL_BASE_URL",
|
|
31
|
+
"INFISCAL_CLIENT_ID",
|
|
32
|
+
"INFISCAL_SECRET",
|
|
33
|
+
"INFISCAL_PATH",
|
|
34
|
+
"INFISCAL_ENV",
|
|
35
|
+
"INFISCAL_PROJECT_ID",
|
|
36
|
+
];
|
|
37
|
+
/**
|
|
38
|
+
* Loads secrets from Infisical using Universal Auth. Bootstrap credentials come
|
|
39
|
+
* from the deployment environment; missing ones produce a ConfigError naming the keys.
|
|
40
|
+
*/
|
|
41
|
+
export function infisicalSource(options = {}) {
|
|
42
|
+
return {
|
|
43
|
+
name: "infisical",
|
|
44
|
+
async load() {
|
|
45
|
+
const env = options.env ?? processEnv();
|
|
46
|
+
const missing = INFISICAL_BOOTSTRAP_KEYS.filter((k) => !env[k]);
|
|
47
|
+
if (missing.length)
|
|
48
|
+
throw new ConfigError("bootstrap_missing", [...missing], `Missing Infisical bootstrap configuration: ${missing.join(", ")}`);
|
|
49
|
+
const base = env.INFISCAL_BASE_URL.replace(/\/+$/, "");
|
|
50
|
+
const doFetch = options.fetch ?? fetch;
|
|
51
|
+
const timeout = options.timeoutMs ?? 10_000;
|
|
52
|
+
const call = async (url, init) => {
|
|
53
|
+
const res = await doFetch(url, { ...init, signal: AbortSignal.timeout(timeout) });
|
|
54
|
+
// Never include response bodies in errors: they may echo secrets.
|
|
55
|
+
if (!res.ok)
|
|
56
|
+
throw new ConfigError("infisical_unavailable", [], `Infisical request failed with HTTP ${res.status}`);
|
|
57
|
+
return (await res.json());
|
|
58
|
+
};
|
|
59
|
+
const login = (await call(`${base}/api/v1/auth/universal-auth/login`, {
|
|
60
|
+
method: "POST",
|
|
61
|
+
headers: { "content-type": "application/json" },
|
|
62
|
+
body: JSON.stringify({ clientId: env.INFISCAL_CLIENT_ID, clientSecret: env.INFISCAL_SECRET }),
|
|
63
|
+
}));
|
|
64
|
+
if (!login.accessToken)
|
|
65
|
+
throw new ConfigError("infisical_auth_failed", [], "Infisical login returned no access token");
|
|
66
|
+
const qs = new URLSearchParams({
|
|
67
|
+
workspaceId: env.INFISCAL_PROJECT_ID,
|
|
68
|
+
environment: env.INFISCAL_ENV,
|
|
69
|
+
secretPath: env.INFISCAL_PATH,
|
|
70
|
+
expandSecretReferences: "true",
|
|
71
|
+
});
|
|
72
|
+
const data = (await call(`${base}/api/v3/secrets/raw?${qs}`, {
|
|
73
|
+
headers: { authorization: `Bearer ${login.accessToken}` },
|
|
74
|
+
}));
|
|
75
|
+
const out = {};
|
|
76
|
+
for (const s of data.secrets ?? [])
|
|
77
|
+
out[s.secretKey] = s.secretValue;
|
|
78
|
+
return out;
|
|
79
|
+
},
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
function schemaKeys(schema) {
|
|
83
|
+
const shape = schema.shape;
|
|
84
|
+
return shape && typeof shape === "object" ? Object.keys(shape).sort() : [];
|
|
85
|
+
}
|
|
86
|
+
export function createSystemConfig(options = {}) {
|
|
87
|
+
let values = null;
|
|
88
|
+
let pending = null;
|
|
89
|
+
let state = "idle";
|
|
90
|
+
let version = 0;
|
|
91
|
+
let loadedAt = null;
|
|
92
|
+
let lastError;
|
|
93
|
+
const registered = new Map();
|
|
94
|
+
const sources = () => options.sources ?? [envSource(), infisicalSource()];
|
|
95
|
+
const status = () => {
|
|
96
|
+
const s = {
|
|
97
|
+
state,
|
|
98
|
+
version,
|
|
99
|
+
loadedAt,
|
|
100
|
+
sources: sources().map((x) => x.name),
|
|
101
|
+
keys: values ? Object.keys(values).sort() : [],
|
|
102
|
+
registered: [...registered.values()],
|
|
103
|
+
};
|
|
104
|
+
if (lastError)
|
|
105
|
+
s.error = lastError;
|
|
106
|
+
return s;
|
|
107
|
+
};
|
|
108
|
+
return {
|
|
109
|
+
load() {
|
|
110
|
+
if (values)
|
|
111
|
+
return Promise.resolve(status());
|
|
112
|
+
if (pending)
|
|
113
|
+
return pending;
|
|
114
|
+
state = "loading";
|
|
115
|
+
pending = (async () => {
|
|
116
|
+
try {
|
|
117
|
+
const merged = {};
|
|
118
|
+
for (const src of sources())
|
|
119
|
+
Object.assign(merged, await src.load());
|
|
120
|
+
values = Object.freeze(merged);
|
|
121
|
+
version++;
|
|
122
|
+
loadedAt = Date.now();
|
|
123
|
+
state = "loaded";
|
|
124
|
+
lastError = undefined;
|
|
125
|
+
return status();
|
|
126
|
+
}
|
|
127
|
+
catch (e) {
|
|
128
|
+
state = "failed";
|
|
129
|
+
lastError = { reason: e instanceof SystemError ? (e.reason ?? "load_failed") : "load_failed", keys: e instanceof ConfigError ? e.keys : [] };
|
|
130
|
+
throw e instanceof ConfigError ? e : new ConfigError("load_failed", [], `Configuration load failed: ${e instanceof Error ? e.name : "error"}`);
|
|
131
|
+
}
|
|
132
|
+
finally {
|
|
133
|
+
pending = null;
|
|
134
|
+
}
|
|
135
|
+
})();
|
|
136
|
+
return pending;
|
|
137
|
+
},
|
|
138
|
+
register(schema, opts = {}) {
|
|
139
|
+
if (!values)
|
|
140
|
+
throw new ConfigError("not_loaded", [], "SystemConfig.register() called before SystemConfig.load() completed");
|
|
141
|
+
const parsed = schema.safeParse(values);
|
|
142
|
+
if (!parsed.success) {
|
|
143
|
+
const keys = [...new Set(parsed.error.issues.map((i) => String(i.path[0] ?? "(root)")))].sort();
|
|
144
|
+
throw new ConfigError("invalid_config", keys, `Invalid or missing configuration keys${opts.moduleId ? ` for ${opts.moduleId}` : ""}: ${keys.join(", ")}`);
|
|
145
|
+
}
|
|
146
|
+
const moduleId = opts.moduleId ?? "app";
|
|
147
|
+
const prev = registered.get(moduleId);
|
|
148
|
+
registered.set(moduleId, { moduleId, keys: [...new Set([...(prev?.keys ?? []), ...schemaKeys(schema)])].sort() });
|
|
149
|
+
return parsed.data;
|
|
150
|
+
},
|
|
151
|
+
status,
|
|
152
|
+
reset() {
|
|
153
|
+
values = null;
|
|
154
|
+
pending = null;
|
|
155
|
+
state = "idle";
|
|
156
|
+
loadedAt = null;
|
|
157
|
+
lastError = undefined;
|
|
158
|
+
registered.clear();
|
|
159
|
+
},
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
/** Process-wide instance: `await SystemConfig.load(); const env = SystemConfig.register(schema)`. */
|
|
163
|
+
const SystemConfig = createSystemConfig();
|
|
164
|
+
export default SystemConfig;
|
|
165
|
+
export { SystemConfig };
|
|
166
|
+
//# sourceMappingURL=index.js.map
|