@palettelab/cli 0.3.52 → 0.3.54
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/README.md +44 -5
- package/docs/python-backend-sdk.md +64 -5
- package/lib/commands/dev.js +35 -2
- package/lib/commands/publish.js +16 -7
- package/lib/dev-simulator.js +33 -1
- package/lib/manifest.js +3 -3
- package/lib/secrets.js +4 -2
- package/package.json +1 -1
- package/platform-dev/docker-compose.yml +4 -2
- package/template-fallback/package.json +1 -1
- package/template-fallback/templates/consumer-app/package.json +1 -1
- package/template-fallback/templates/dashboard/package.json +1 -1
- package/template-fallback/templates/database/package.json +1 -1
- package/template-fallback/templates/external-service/package.json +1 -1
- package/template-fallback/templates/frontend-only/package.json +1 -1
- package/template-fallback/templates/next/package.json +1 -1
- package/template-fallback/templates/palette-app/package.json +1 -1
- package/template-fallback/templates/provider-app/package.json +1 -1
package/README.md
CHANGED
|
@@ -502,11 +502,14 @@ work without Docker or platform source. The terminal streams local frontend
|
|
|
502
502
|
requests, frontend rebuilds, and backend process output while `pltt dev` is
|
|
503
503
|
running.
|
|
504
504
|
|
|
505
|
-
The simulator also provides the same
|
|
506
|
-
`language`, `fallbackLanguage`, `supportedLanguages`,
|
|
507
|
-
Generated frontend templates include
|
|
508
|
-
files wired through
|
|
509
|
-
|
|
505
|
+
The simulator also provides the same OS context fields that Palette OS provides:
|
|
506
|
+
`language`, `fallbackLanguage`, `supportedLanguages`, `setLanguage()`,
|
|
507
|
+
`colorMode`, and `setColorMode()`. Generated frontend templates include
|
|
508
|
+
app-owned `frontend/src/translations.ts` files wired through
|
|
509
|
+
`usePluginTranslations()`, so apps can switch when the OS language changes
|
|
510
|
+
without storing copy in the platform. Apps can read `usePlatform().colorMode`
|
|
511
|
+
or listen for the `palette:theme-change` browser event to react when Palette OS
|
|
512
|
+
changes between light and dark mode.
|
|
510
513
|
|
|
511
514
|
For Python apps with database tables, `ctx.db` is an async SQLAlchemy session in
|
|
512
515
|
local dev. The simulator imports `backend/api/models.py` when present and creates
|
|
@@ -783,6 +786,10 @@ If no plugin ID is provided, the CLI uses the current `palette-plugin.json` or `
|
|
|
783
786
|
Inspect and generate OS-broker service integrations declared through
|
|
784
787
|
`provides` and `consumes` in `palette-plugin.json`.
|
|
785
788
|
|
|
789
|
+
Use this command family when one Palette app needs governed data, approvals, or
|
|
790
|
+
events from another app. The CLI keeps the app manifest, local mocks, provider
|
|
791
|
+
schema files, and generated clients aligned with the broker contract.
|
|
792
|
+
|
|
786
793
|
```bash
|
|
787
794
|
pltt init employee-management --template provider-app
|
|
788
795
|
pltt init leave-management --template consumer-app
|
|
@@ -810,6 +817,38 @@ Generated TypeScript clients use the SDK's default `palette` client. Generated
|
|
|
810
817
|
Python clients call `palette_sdk.services(ctx)`, so backend routes still pass
|
|
811
818
|
through Palette's broker permission and install checks.
|
|
812
819
|
|
|
820
|
+
Recommended workflow:
|
|
821
|
+
|
|
822
|
+
1. Provider app: implement backend methods with `@service(...)` and declare
|
|
823
|
+
event topics in `provides.events`.
|
|
824
|
+
2. Provider app: run `pltt services sync` or `pltt services scaffold <method>`
|
|
825
|
+
to keep manifest methods and schema files current.
|
|
826
|
+
3. Consumer app: run `pltt services add <target>` for each consumed service or
|
|
827
|
+
event, including a `--reason` for review and install UI.
|
|
828
|
+
4. Consumer app: use `pltt services mock <target>` and `pltt services test
|
|
829
|
+
--offline` during local development.
|
|
830
|
+
5. Consumer app: use `pltt services pull --env staging` to generate typed
|
|
831
|
+
clients from the live `/api/v1/os-broker/schemas` endpoint.
|
|
832
|
+
6. Run `pltt test` before publish; contract checks include local
|
|
833
|
+
`provides`/`consumes`, mocks, schemas, and dependency-policy validation.
|
|
834
|
+
|
|
835
|
+
Runtime behavior:
|
|
836
|
+
|
|
837
|
+
- Service calls go through `POST /api/v1/os-broker/dispatch`.
|
|
838
|
+
- Event publishes go through `POST /api/v1/os-broker/events/emit`.
|
|
839
|
+
- Event subscriptions use `GET /api/v1/os-broker/events/stream`.
|
|
840
|
+
- Catalog and generated client metadata come from `/api/v1/os-broker/catalog`
|
|
841
|
+
and `/api/v1/os-broker/schemas`.
|
|
842
|
+
- Installs use `/api/v1/app-installs/<app_id>/dependency-plan` and can include
|
|
843
|
+
required provider apps with `?include_dependencies=true`.
|
|
844
|
+
- Org owners/admins can revoke or restore individual cross-app grants from
|
|
845
|
+
Settings > Apps; the broker checks those grants on every call and stream.
|
|
846
|
+
|
|
847
|
+
Provider methods can use inline JSON Schema objects or schema file paths in
|
|
848
|
+
`input_schema`, `input`, `output_schema`, and `output`. Event topics can use
|
|
849
|
+
`schema` or `payload_schema`. The CLI scaffolds schema files by default because
|
|
850
|
+
they are easier to review and reuse for generated clients.
|
|
851
|
+
|
|
813
852
|
## Global Flags
|
|
814
853
|
|
|
815
854
|
- `--json` emits machine-readable output for `package`, `publish`, `status`, `logs`, `test`, and `version`.
|
|
@@ -693,7 +693,18 @@ token = await ctx.connections.access_token("google_calendar")
|
|
|
693
693
|
## 11. App-To-App Services
|
|
694
694
|
|
|
695
695
|
Apps can expose governed broker services and events, then consume them from
|
|
696
|
-
other installed apps without knowing another app's URL.
|
|
696
|
+
other installed apps without knowing another app's URL. The broker is the only
|
|
697
|
+
supported integration path for cross-app business data. Do not read another
|
|
698
|
+
app's database tables and do not call another app's private backend route
|
|
699
|
+
directly.
|
|
700
|
+
|
|
701
|
+
Palette enforces the app-to-app contract in four places:
|
|
702
|
+
|
|
703
|
+
- Manifest review: providers declare `provides`, consumers declare `consumes`.
|
|
704
|
+
- Install: required provider apps are resolved before the consumer is activated.
|
|
705
|
+
- Org grants: owners/admins can allow or revoke each consumed service/event.
|
|
706
|
+
- Runtime: every service call, event emit, and event stream is checked against
|
|
707
|
+
same-org install state, `consumes`, grants, and JSON Schemas.
|
|
697
708
|
|
|
698
709
|
Provider apps declare a namespace and callable methods with `provides`:
|
|
699
710
|
|
|
@@ -705,25 +716,40 @@ Provider apps declare a namespace and callable methods with `provides`:
|
|
|
705
716
|
{
|
|
706
717
|
"id": "hr.directory",
|
|
707
718
|
"methods": [
|
|
708
|
-
{
|
|
719
|
+
{
|
|
720
|
+
"name": "approvalChain.get",
|
|
721
|
+
"input_schema": "schemas/approval-chain.input.json",
|
|
722
|
+
"output_schema": "schemas/approval-chain.output.json"
|
|
723
|
+
}
|
|
709
724
|
]
|
|
710
725
|
}
|
|
711
726
|
],
|
|
712
|
-
"events": [
|
|
727
|
+
"events": [
|
|
728
|
+
{
|
|
729
|
+
"topic": "hierarchy.updated",
|
|
730
|
+
"schema": "schemas/hierarchy-updated.json"
|
|
731
|
+
}
|
|
732
|
+
]
|
|
713
733
|
}
|
|
714
734
|
}
|
|
715
735
|
```
|
|
716
736
|
|
|
717
|
-
Expose the handler with `@service
|
|
737
|
+
Expose the handler with `@service`. The platform registers handlers when the
|
|
738
|
+
plugin loads and injects a full `PluginContext` at dispatch time:
|
|
718
739
|
|
|
719
740
|
```python
|
|
720
741
|
from palette_sdk import PluginContext, service
|
|
721
742
|
|
|
722
|
-
@service("approvalChain.get")
|
|
743
|
+
@service("approvalChain.get", scope="members:read")
|
|
723
744
|
async def approval_chain(ctx: PluginContext, payload: dict) -> dict:
|
|
724
745
|
return {"approvers": [{"user_id": ctx.user_id, "step": 1}]}
|
|
725
746
|
```
|
|
726
747
|
|
|
748
|
+
If a method also declares `route_method` and `route_path`, Palette can fall back
|
|
749
|
+
to signed internal HTTP transport when no in-process handler is registered. New
|
|
750
|
+
provider apps should prefer `@service(...)` because it avoids URL coupling and
|
|
751
|
+
keeps dispatch inside the broker lifecycle.
|
|
752
|
+
|
|
727
753
|
Consumers declare qualified targets with `consumes`:
|
|
728
754
|
|
|
729
755
|
```json
|
|
@@ -746,8 +772,11 @@ Consumers declare qualified targets with `consumes`:
|
|
|
746
772
|
The CLI can update the manifest and generate typed clients for those targets:
|
|
747
773
|
|
|
748
774
|
```bash
|
|
775
|
+
pltt services sync
|
|
749
776
|
pltt services add hr/v1#approvalChain.get --reason "Route leave approvals through HR"
|
|
750
777
|
pltt services add hr/v1#hierarchy.updated --event --optional
|
|
778
|
+
pltt services mock hr/v1#approvalChain.get
|
|
779
|
+
pltt services test --offline
|
|
751
780
|
pltt services pull --env staging
|
|
752
781
|
```
|
|
753
782
|
|
|
@@ -764,12 +793,42 @@ from palette_sdk import services
|
|
|
764
793
|
|
|
765
794
|
chain = await services(ctx).call("hr/v1#approvalChain.get", {"user_id": ctx.user_id})
|
|
766
795
|
await ctx.events.emit("leave/v1#leave.requested", {"approval_chain": chain})
|
|
796
|
+
await ctx.events.emit_durable("leave/v1#leave.requested", {"approval_chain": chain})
|
|
767
797
|
```
|
|
768
798
|
|
|
769
799
|
At install time, Palette checks required `consumes` targets and can show the
|
|
770
800
|
provider apps that must also be installed. The org-level grant is saved on the
|
|
771
801
|
install and the broker checks it on every call, emit, or event stream.
|
|
772
802
|
|
|
803
|
+
Useful platform endpoints when building or debugging integrations:
|
|
804
|
+
|
|
805
|
+
```text
|
|
806
|
+
GET /api/v1/app-installs/{app_id}/dependency-plan
|
|
807
|
+
POST /api/v1/app-installs/{app_id}?include_dependencies=true
|
|
808
|
+
GET /api/v1/app-installs/{app_id}/cross-app-grants
|
|
809
|
+
PATCH /api/v1/app-installs/{app_id}/cross-app-grants
|
|
810
|
+
GET /api/v1/os-broker/catalog
|
|
811
|
+
GET /api/v1/os-broker/schemas?targets=hr/v1#approvalChain.get
|
|
812
|
+
GET /api/v1/os-broker/audit?app_id=leave-management
|
|
813
|
+
```
|
|
814
|
+
|
|
815
|
+
Local simulator workflows can use `.palette/app-services.local.json` mocks.
|
|
816
|
+
The mock command writes the file for you:
|
|
817
|
+
|
|
818
|
+
```bash
|
|
819
|
+
pltt services mock hr/v1#approvalChain.get
|
|
820
|
+
```
|
|
821
|
+
|
|
822
|
+
Example mock:
|
|
823
|
+
|
|
824
|
+
```json
|
|
825
|
+
{
|
|
826
|
+
"hr/v1#approvalChain.get": {
|
|
827
|
+
"approvers": [{ "user_id": "manager-1", "step": 1 }]
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
```
|
|
831
|
+
|
|
773
832
|
App storage is separate from Data Rooms. Use `ctx.storage` and `palette.storage` for app-owned files that go directly to the OS-configured storage backend, currently GCS in hosted environments. Use `ctx.data_rooms` or `palette.dataRooms` only when the file should be visible and governed as a Data Room document.
|
|
774
833
|
|
|
775
834
|
Palette scopes storage the same way. Files written through `ctx.storage` or the
|
package/lib/commands/dev.js
CHANGED
|
@@ -8,7 +8,7 @@ const { watchFrontend } = require("../bundler")
|
|
|
8
8
|
const { parseFlags, resolveEnvironment } = require("../environments")
|
|
9
9
|
const { resolveDevPorts } = require("../ports")
|
|
10
10
|
const { startSimulator } = require("../dev-simulator")
|
|
11
|
-
const {
|
|
11
|
+
const { loadLocalEnvDetails } = require("../secrets")
|
|
12
12
|
const buildCommand = require("./build")
|
|
13
13
|
const publish = require("./publish")
|
|
14
14
|
const logs = require("./logs")
|
|
@@ -98,11 +98,39 @@ function lintMigrationsForDev(cwd, manifest) {
|
|
|
98
98
|
process.exit(1)
|
|
99
99
|
}
|
|
100
100
|
|
|
101
|
+
function formatEnvValue(value) {
|
|
102
|
+
if (value === undefined || value === null) return ""
|
|
103
|
+
const str = String(value)
|
|
104
|
+
if (!str) return ""
|
|
105
|
+
if (/[\s#"'\\]/.test(str)) return JSON.stringify(str)
|
|
106
|
+
return str
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function writePlatformEnvFile(cwd, localEnv) {
|
|
110
|
+
const dir = path.join(cwd, ".palette")
|
|
111
|
+
fs.mkdirSync(dir, { recursive: true })
|
|
112
|
+
const envPath = path.join(dir, "platform.env")
|
|
113
|
+
const values = {}
|
|
114
|
+
for (const [key, value] of Object.entries(localEnv?.values || {})) {
|
|
115
|
+
values[key] = process.env[key] !== undefined ? process.env[key] : value
|
|
116
|
+
}
|
|
117
|
+
const lines = [
|
|
118
|
+
"# Generated by pltt dev --platform from local env files.",
|
|
119
|
+
"# Do not commit this file.",
|
|
120
|
+
"",
|
|
121
|
+
]
|
|
122
|
+
for (const [key, value] of Object.entries(values).sort(([a], [b]) => a.localeCompare(b))) {
|
|
123
|
+
lines.push(`${key}=${formatEnvValue(value)}`)
|
|
124
|
+
}
|
|
125
|
+
fs.writeFileSync(envPath, `${lines.join("\n")}\n`)
|
|
126
|
+
return envPath
|
|
127
|
+
}
|
|
128
|
+
|
|
101
129
|
async function run(args, { cwd }) {
|
|
102
130
|
const { flags, rest } = parseFlags(args)
|
|
103
131
|
const cloud = rest.includes("--cloud") || rest.includes("--sandbox")
|
|
104
132
|
const platform = rest.includes("--platform")
|
|
105
|
-
|
|
133
|
+
const localEnv = loadLocalEnvDetails(cwd, { environment: flags.env })
|
|
106
134
|
if (cloud) {
|
|
107
135
|
const json = args.includes("--json")
|
|
108
136
|
const publishArgs = ["--publish-type", "preview"]
|
|
@@ -199,6 +227,9 @@ async function run(args, { cwd }) {
|
|
|
199
227
|
}
|
|
200
228
|
console.log(`[pltt] frontend: http://localhost:${frontendPort}/apps/${pluginId}`)
|
|
201
229
|
console.log(`[pltt] backend: http://localhost:${backendPort}/api/v1/plugins/${pluginId}`)
|
|
230
|
+
if (localEnv.files.length) {
|
|
231
|
+
console.log(`[pltt] env files: ${localEnv.files.join(", ")}`)
|
|
232
|
+
}
|
|
202
233
|
|
|
203
234
|
// Pre-pull so we can give a useful error if the image isn't reachable
|
|
204
235
|
// (common cause: maintainer hasn't pushed it yet, or `docker login ghcr.io`
|
|
@@ -210,11 +241,13 @@ async function run(args, { cwd }) {
|
|
|
210
241
|
process.exit(1)
|
|
211
242
|
}
|
|
212
243
|
|
|
244
|
+
const platformEnvFile = writePlatformEnvFile(cwd, localEnv)
|
|
213
245
|
const env = {
|
|
214
246
|
...process.env,
|
|
215
247
|
PALETTE_DEV_IMAGE: DEFAULT_IMAGE,
|
|
216
248
|
PALETTE_ACTIVE_PLUGIN: pluginId,
|
|
217
249
|
PALETTE_PLUGIN_DIR: cwd,
|
|
250
|
+
PALETTE_PLUGIN_ENV_FILE: platformEnvFile,
|
|
218
251
|
PALETTE_FRONTEND_PORT: frontendPort,
|
|
219
252
|
PALETTE_BACKEND_PORT: backendPort,
|
|
220
253
|
}
|
package/lib/commands/publish.js
CHANGED
|
@@ -228,6 +228,14 @@ function scopesOf(spec) {
|
|
|
228
228
|
return ["dev"]
|
|
229
229
|
}
|
|
230
230
|
|
|
231
|
+
function withPluginScope(spec) {
|
|
232
|
+
const scopes = Array.from(new Set([...scopesOf(spec), "plugin"]))
|
|
233
|
+
return {
|
|
234
|
+
...spec,
|
|
235
|
+
scope: scopes.length === 1 ? scopes[0] : scopes,
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
231
239
|
function cloneManifest(manifest) {
|
|
232
240
|
return JSON.parse(JSON.stringify(manifest))
|
|
233
241
|
}
|
|
@@ -235,12 +243,6 @@ function cloneManifest(manifest) {
|
|
|
235
243
|
function collectPluginSecrets(cwd, manifest, env, flags, log, localEnv) {
|
|
236
244
|
const declared = declaredSecrets(manifest)
|
|
237
245
|
const pluginSecrets = Object.entries(declared).filter(([, spec]) => spec.scope.includes("plugin"))
|
|
238
|
-
const devRequired = Object.entries(declared).filter(([, spec]) => spec.scope.includes("dev") && spec.required)
|
|
239
|
-
if (devRequired.length) {
|
|
240
|
-
log(
|
|
241
|
-
`[pltt] dev-only secrets are not uploaded: ${devRequired.map(([name]) => name).join(", ")}`,
|
|
242
|
-
)
|
|
243
|
-
}
|
|
244
246
|
|
|
245
247
|
let fileValues = {}
|
|
246
248
|
if (flags.secretsFile) {
|
|
@@ -281,7 +283,14 @@ function collectPluginSecrets(cwd, manifest, env, flags, log, localEnv) {
|
|
|
281
283
|
}
|
|
282
284
|
const explicitSpec = effectiveManifest.secrets[name]
|
|
283
285
|
if (explicitSpec) {
|
|
284
|
-
|
|
286
|
+
const value = fileValues[name] ?? process.env[name] ?? localValues[name]
|
|
287
|
+
if (scopesOf(explicitSpec).includes("plugin")) {
|
|
288
|
+
values[name] = value
|
|
289
|
+
} else if (canAutoUploadEnvKey(name)) {
|
|
290
|
+
effectiveManifest.secrets[name] = withPluginScope(explicitSpec)
|
|
291
|
+
values[name] = value
|
|
292
|
+
autoUploaded.push(name)
|
|
293
|
+
}
|
|
285
294
|
continue
|
|
286
295
|
}
|
|
287
296
|
if (isReservedAutoEnvKey(name)) {
|
package/lib/dev-simulator.js
CHANGED
|
@@ -576,6 +576,21 @@ function normalizePaletteLanguage(language, fallback = "en") {
|
|
|
576
576
|
return value ? value.split("-")[0] : fallback
|
|
577
577
|
}
|
|
578
578
|
|
|
579
|
+
function normalizePaletteColorMode(mode, fallback = "light") {
|
|
580
|
+
return mode === "dark" ? "dark" : fallback
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
function detectPaletteColorMode() {
|
|
584
|
+
return window.matchMedia?.("(prefers-color-scheme: dark)")?.matches ? "dark" : "light"
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
function applyPaletteColorMode(mode) {
|
|
588
|
+
const normalized = normalizePaletteColorMode(mode)
|
|
589
|
+
document.documentElement.classList.toggle("dark", normalized === "dark")
|
|
590
|
+
document.documentElement.style.colorScheme = normalized
|
|
591
|
+
window.dispatchEvent(new CustomEvent("palette:theme-change", { detail: { colorMode: normalized } }))
|
|
592
|
+
}
|
|
593
|
+
|
|
579
594
|
function Toasts() {
|
|
580
595
|
const [items, setItems] = React.useState([])
|
|
581
596
|
React.useEffect(() => {
|
|
@@ -594,6 +609,7 @@ function Toasts() {
|
|
|
594
609
|
|
|
595
610
|
function Shell() {
|
|
596
611
|
const [language, updateLanguage] = React.useState(() => normalizePaletteLanguage(navigator.language))
|
|
612
|
+
const [colorMode, updateColorMode] = React.useState(() => detectPaletteColorMode())
|
|
597
613
|
const platform = React.useMemo(() => ({
|
|
598
614
|
...basePlatform,
|
|
599
615
|
language,
|
|
@@ -605,18 +621,34 @@ function Shell() {
|
|
|
605
621
|
document.documentElement.lang = normalized
|
|
606
622
|
window.dispatchEvent(new CustomEvent("palette:language-change", { detail: { language: normalized } }))
|
|
607
623
|
},
|
|
608
|
-
|
|
624
|
+
colorMode,
|
|
625
|
+
setColorMode: (nextMode) => {
|
|
626
|
+
updateColorMode(normalizePaletteColorMode(nextMode))
|
|
627
|
+
},
|
|
628
|
+
}), [language, colorMode])
|
|
609
629
|
|
|
610
630
|
React.useEffect(() => {
|
|
611
631
|
document.documentElement.lang = language
|
|
612
632
|
}, [language])
|
|
613
633
|
|
|
634
|
+
React.useEffect(() => {
|
|
635
|
+
applyPaletteColorMode(colorMode)
|
|
636
|
+
}, [colorMode])
|
|
637
|
+
|
|
614
638
|
React.useEffect(() => {
|
|
615
639
|
const onLanguageChange = () => updateLanguage(normalizePaletteLanguage(navigator.language))
|
|
616
640
|
window.addEventListener("languagechange", onLanguageChange)
|
|
617
641
|
return () => window.removeEventListener("languagechange", onLanguageChange)
|
|
618
642
|
}, [])
|
|
619
643
|
|
|
644
|
+
React.useEffect(() => {
|
|
645
|
+
const media = window.matchMedia?.("(prefers-color-scheme: dark)")
|
|
646
|
+
if (!media) return
|
|
647
|
+
const onColorModeChange = () => updateColorMode(detectPaletteColorMode())
|
|
648
|
+
media.addEventListener?.("change", onColorModeChange)
|
|
649
|
+
return () => media.removeEventListener?.("change", onColorModeChange)
|
|
650
|
+
}, [])
|
|
651
|
+
|
|
620
652
|
return React.createElement(PluginProvider, { value: platform },
|
|
621
653
|
React.createElement("main", { className: "palette-local-shell" },
|
|
622
654
|
React.createElement(Plugin, { platform }),
|
package/lib/manifest.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
const fs = require("fs")
|
|
4
4
|
const path = require("path")
|
|
5
|
-
const { SECRET_SCOPES } = require("./secrets")
|
|
5
|
+
const { ENV_KEY_PATTERN, SECRET_SCOPES } = require("./secrets")
|
|
6
6
|
|
|
7
7
|
const MANIFEST_FILE = "palette-plugin.json"
|
|
8
8
|
const SUPPORTED_MANIFEST_VERSIONS = ["1"]
|
|
@@ -127,8 +127,8 @@ function validateSecrets(value, errors) {
|
|
|
127
127
|
const allowed = new Set(["scope", "required", "label", "help", "validate"])
|
|
128
128
|
for (const [name, spec] of Object.entries(value)) {
|
|
129
129
|
const label = `secrets.${name}`
|
|
130
|
-
if (
|
|
131
|
-
errors.push(`${label} must be
|
|
130
|
+
if (!ENV_KEY_PATTERN.test(name)) {
|
|
131
|
+
errors.push(`${label} must be a valid environment variable name`)
|
|
132
132
|
}
|
|
133
133
|
if (!isObject(spec)) {
|
|
134
134
|
errors.push(`${label} must be an object`)
|
package/lib/secrets.js
CHANGED
|
@@ -6,6 +6,7 @@ const path = require("path")
|
|
|
6
6
|
const LOCAL_ENV_PATH = path.join(".palette", ".env.local")
|
|
7
7
|
const EXAMPLE_ENV_PATH = path.join(".palette", ".env.example")
|
|
8
8
|
const SECRET_SCOPES = new Set(["dev", "plugin", "install", "platform"])
|
|
9
|
+
const ENV_KEY_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/
|
|
9
10
|
const RESERVED_AUTO_ENV_KEYS = new Set([
|
|
10
11
|
"CI",
|
|
11
12
|
"HOME",
|
|
@@ -162,7 +163,7 @@ function declaredSecrets(manifest) {
|
|
|
162
163
|
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return {}
|
|
163
164
|
const out = {}
|
|
164
165
|
for (const [name, meta] of Object.entries(raw)) {
|
|
165
|
-
if (
|
|
166
|
+
if (!ENV_KEY_PATTERN.test(name)) continue
|
|
166
167
|
const item = meta && typeof meta === "object" && !Array.isArray(meta) ? meta : {}
|
|
167
168
|
out[name] = {
|
|
168
169
|
...item,
|
|
@@ -210,7 +211,7 @@ function isReservedAutoEnvKey(key) {
|
|
|
210
211
|
}
|
|
211
212
|
|
|
212
213
|
function canAutoUploadEnvKey(key) {
|
|
213
|
-
return
|
|
214
|
+
return ENV_KEY_PATTERN.test(key) && !isPublicEnvKey(key) && !isReservedAutoEnvKey(key)
|
|
214
215
|
}
|
|
215
216
|
|
|
216
217
|
function redactValue(value) {
|
|
@@ -222,6 +223,7 @@ function redactValue(value) {
|
|
|
222
223
|
|
|
223
224
|
module.exports = {
|
|
224
225
|
EXAMPLE_ENV_PATH,
|
|
226
|
+
ENV_KEY_PATTERN,
|
|
225
227
|
LOCAL_ENV_PATH,
|
|
226
228
|
SECRET_SCOPES,
|
|
227
229
|
declaredSecrets,
|
package/package.json
CHANGED
|
@@ -9,6 +9,8 @@
|
|
|
9
9
|
services:
|
|
10
10
|
platform:
|
|
11
11
|
image: ${PALETTE_DEV_IMAGE:-ghcr.io/palette-lab/platform-dev:latest}
|
|
12
|
+
env_file:
|
|
13
|
+
- ${PALETTE_PLUGIN_ENV_FILE:-/dev/null}
|
|
12
14
|
ports:
|
|
13
15
|
- "${PALETTE_FRONTEND_PORT:-7321}:3000"
|
|
14
16
|
- "${PALETTE_BACKEND_PORT:-8732}:8000"
|
|
@@ -25,8 +27,8 @@ services:
|
|
|
25
27
|
STORAGE_BACKEND: "local"
|
|
26
28
|
LOCAL_STORAGE_DIR: "/srv/storage"
|
|
27
29
|
# Disable optional features that need real credentials
|
|
28
|
-
RAG_ENABLED: "false"
|
|
29
|
-
OPENAI_API_KEY: ""
|
|
30
|
+
RAG_ENABLED: "${RAG_ENABLED:-false}"
|
|
31
|
+
OPENAI_API_KEY: "${OPENAI_API_KEY:-}"
|
|
30
32
|
volumes:
|
|
31
33
|
- "${PALETTE_PLUGIN_DIR}:/plugins/${PALETTE_ACTIVE_PLUGIN}"
|
|
32
34
|
depends_on:
|