@malloydata/db-duckdb 0.0.379 → 0.0.380
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/MAINTENANCE_NOTES.md +70 -70
- package/dist/duckdb_config.d.ts +2 -5
- package/dist/duckdb_config.js +52 -52
- package/dist/duckdb_connection.d.ts +1 -3
- package/dist/duckdb_connection.js +2 -8
- package/dist/native.js +2 -9
- package/package.json +2 -2
package/MAINTENANCE_NOTES.md
CHANGED
|
@@ -10,20 +10,29 @@ must not depend on them for essential context.
|
|
|
10
10
|
|
|
11
11
|
## Mental Model
|
|
12
12
|
|
|
13
|
-
Malloy exposes
|
|
13
|
+
Malloy exposes one user-facing policy property for native DuckDB:
|
|
14
14
|
|
|
15
|
-
- `
|
|
16
|
-
- `networkPolicy: "open" | "closed"`
|
|
15
|
+
- `securityPolicy: "none" | "local" | "sandboxed"`
|
|
17
16
|
|
|
18
|
-
|
|
19
|
-
and
|
|
17
|
+
This field is a Malloy policy control. It is not a raw DuckDB option name
|
|
18
|
+
and it is not a general policy language.
|
|
20
19
|
|
|
21
|
-
The
|
|
20
|
+
The three levels form a strict escalation:
|
|
22
21
|
|
|
23
|
-
- `
|
|
24
|
-
- `
|
|
22
|
+
- `"none"` — no security policy applied. Ordinary DuckDB behavior.
|
|
23
|
+
- `"local"` — no network access. DuckDB cannot reach the network, but local
|
|
24
|
+
filesystem access is not sandboxed to specific directories.
|
|
25
|
+
- `"sandboxed"` — no network access AND filesystem confined to
|
|
26
|
+
`allowedDirectories`. The reviewed strict recipe.
|
|
25
27
|
|
|
26
|
-
|
|
28
|
+
DuckDB's `enable_external_access` is a single toggle that gates both filesystem
|
|
29
|
+
reach beyond the working directory and network reach. Setting
|
|
30
|
+
`allowed_directories` without disabling `enable_external_access` has no effect.
|
|
31
|
+
This is why the policy is a single axis rather than two independent axes — the
|
|
32
|
+
underlying DuckDB mechanism does not support independent filesystem and network
|
|
33
|
+
control.
|
|
34
|
+
|
|
35
|
+
The implementation compiles the public policy value into an internal
|
|
27
36
|
`NormalizedDuckDBSafetyPolicy`. That internal object records the derived
|
|
28
37
|
enforcement requirements, such as locked configuration, no setup SQL,
|
|
29
38
|
temp-file encryption, extension restrictions, and secret neutralization. Keep
|
|
@@ -41,9 +50,9 @@ process isolation, query cancellation, and host-level quotas separately.
|
|
|
41
50
|
|
|
42
51
|
- Native connection schema: `src/native.ts`
|
|
43
52
|
- Registers native DuckDB properties.
|
|
44
|
-
- `
|
|
45
|
-
|
|
46
|
-
|
|
53
|
+
- `securityPolicy` is `requireLiteralString` so invalid reference-shaped or
|
|
54
|
+
non-string values reach registry validation instead of being silently
|
|
55
|
+
dropped by generic config compilation.
|
|
47
56
|
- Config compiler literal guard: `../malloy/src/api/foundation/config_compile.ts`
|
|
48
57
|
- Preserves invalid literal-required values as values after warning, allowing
|
|
49
58
|
registry lookup to fail closed before the DuckDB factory runs.
|
|
@@ -72,7 +81,7 @@ process isolation, query cancellation, and host-level quotas separately.
|
|
|
72
81
|
baseline must fail connection creation.
|
|
73
82
|
- Policy validation must run early enough that invalid raw values such as
|
|
74
83
|
`true`, `42`, or `{env: "POLICY"}` cannot be silently dropped and interpreted
|
|
75
|
-
as the default `"
|
|
84
|
+
as the default `"none"` policy.
|
|
76
85
|
- A restricted DuckDB connection must never share a live instance with a less
|
|
77
86
|
restrictive or otherwise semantically different connection.
|
|
78
87
|
- The fixed baseline must be established before any Malloy-derived SQL runs.
|
|
@@ -85,22 +94,11 @@ process isolation, query cancellation, and host-level quotas separately.
|
|
|
85
94
|
|
|
86
95
|
## User-Facing Policy Behavior
|
|
87
96
|
|
|
88
|
-
`
|
|
97
|
+
`securityPolicy: "none"` means no security policy is applied. Ordinary DuckDB
|
|
98
|
+
behavior.
|
|
89
99
|
|
|
90
|
-
`
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
- `allowedDirectories` is required or derived.
|
|
94
|
-
- `tempDirectory` is required or derived.
|
|
95
|
-
- `workingDirectory`, when present, must be inside `allowedDirectories`.
|
|
96
|
-
- `tempDirectory` must be inside `allowedDirectories`.
|
|
97
|
-
- Non-POSIX hosts are rejected. Do not approximate Windows path behavior for
|
|
98
|
-
the current policy.
|
|
99
|
-
|
|
100
|
-
`networkPolicy: "open"` means network-capable DuckDB behavior may remain
|
|
101
|
-
available.
|
|
102
|
-
|
|
103
|
-
`networkPolicy: "closed"` means Malloy disables network-capable DuckDB behavior:
|
|
100
|
+
`securityPolicy: "local"` means Malloy disables network-capable DuckDB
|
|
101
|
+
behavior:
|
|
104
102
|
|
|
105
103
|
- `enableExternalAccess` is forced to `false`.
|
|
106
104
|
- `httpfs` must not load.
|
|
@@ -108,14 +106,20 @@ available.
|
|
|
108
106
|
- network-requiring `databasePath` values, including MotherDuck paths, are
|
|
109
107
|
rejected.
|
|
110
108
|
- `motherDuckToken` is rejected.
|
|
109
|
+
- configuration is locked after baseline setup.
|
|
110
|
+
- `setupSQL` is not allowed.
|
|
111
|
+
- temp-file encryption is required.
|
|
112
|
+
- secrets are neutralized.
|
|
111
113
|
|
|
112
|
-
|
|
114
|
+
`securityPolicy: "sandboxed"` means everything in `"local"`, plus Malloy
|
|
115
|
+
derives and enforces a native DuckDB filesystem boundary:
|
|
113
116
|
|
|
114
|
-
-
|
|
115
|
-
|
|
116
|
-
-
|
|
117
|
-
|
|
118
|
-
|
|
117
|
+
- `allowedDirectories` is required or derived.
|
|
118
|
+
- `tempDirectory` is required or derived.
|
|
119
|
+
- `workingDirectory`, when present, must be inside `allowedDirectories`.
|
|
120
|
+
- `tempDirectory` must be inside `allowedDirectories`.
|
|
121
|
+
- Non-POSIX hosts are rejected. Do not approximate Windows path behavior for
|
|
122
|
+
the current policy.
|
|
119
123
|
|
|
120
124
|
## Registered Config Surface
|
|
121
125
|
|
|
@@ -128,10 +132,9 @@ Native DuckDB supports the existing Malloy-facing properties:
|
|
|
128
132
|
- `readOnly`
|
|
129
133
|
- `setupSQL`
|
|
130
134
|
|
|
131
|
-
The config extension adds policy
|
|
135
|
+
The config extension adds the policy property:
|
|
132
136
|
|
|
133
|
-
- `
|
|
134
|
-
- `networkPolicy`
|
|
137
|
+
- `securityPolicy`
|
|
135
138
|
|
|
136
139
|
It also adds curated DuckDB-level properties:
|
|
137
140
|
|
|
@@ -153,7 +156,7 @@ connection property model does not yet have a first-class string-array type.
|
|
|
153
156
|
Even though the registry metadata says `json`, DuckDB normalization must require
|
|
154
157
|
the value to be a JSON array of strings. Do not add a registry default for
|
|
155
158
|
`allowedDirectories`; its only defaulting behavior belongs to
|
|
156
|
-
`
|
|
159
|
+
`securityPolicy: "sandboxed"` normalization.
|
|
157
160
|
|
|
158
161
|
`workingDirectory` defaults to `{config: "rootDirectory"}` in the native DuckDB
|
|
159
162
|
registry. Hosts that know the project root should populate `config.rootDirectory`
|
|
@@ -171,28 +174,28 @@ All policy reasoning belongs in `normalizeDuckDBConfig()`.
|
|
|
171
174
|
|
|
172
175
|
Policy parsing:
|
|
173
176
|
|
|
174
|
-
- Missing `
|
|
175
|
-
-
|
|
176
|
-
|
|
177
|
+
- Missing `securityPolicy` defaults to `"none"`.
|
|
178
|
+
- Accepted policy values are exact documented strings only: `"none"`, `"local"`,
|
|
179
|
+
`"sandboxed"`.
|
|
177
180
|
- Unknown strings, strings with whitespace or different casing, non-strings,
|
|
178
181
|
and reference-shaped values must fail closed.
|
|
179
182
|
|
|
180
183
|
Derived safety policy:
|
|
181
184
|
|
|
182
|
-
- If
|
|
185
|
+
- If `securityPolicy` is `"local"` or `"sandboxed"` (i.e., restricted), derive:
|
|
183
186
|
- `requiresLockedConfiguration: true`
|
|
184
187
|
- `requiresNoSetupSQL: true`
|
|
185
188
|
- `requiresTempFileEncryption: true`
|
|
186
189
|
- `requiresSecretNeutralization: true`
|
|
187
190
|
- `forbidAdditionalExtensions: true`
|
|
191
|
+
- `allowHttpfs: false`
|
|
192
|
+
- `enableExternalAccess: false`
|
|
188
193
|
- required baseline extensions `icu` and `json`
|
|
189
|
-
-
|
|
194
|
+
- no extension install or auto-install/autoload expansion
|
|
195
|
+
- If `securityPolicy === "sandboxed"`, also derive:
|
|
190
196
|
- POSIX host required
|
|
191
197
|
- sandboxed path validation required
|
|
192
198
|
- derived temp directory name `.tmp`
|
|
193
|
-
- If `networkPolicy === "closed"`, also derive:
|
|
194
|
-
- `allowHttpfs: false`
|
|
195
|
-
- no extension install or auto-install/autoload expansion
|
|
196
199
|
|
|
197
200
|
Conflict checks:
|
|
198
201
|
|
|
@@ -201,7 +204,7 @@ Conflict checks:
|
|
|
201
204
|
extension broadening.
|
|
202
205
|
- Reject `lockConfiguration: false` under any restricted policy.
|
|
203
206
|
- Reject `tempFileEncryption: false` under any restricted policy.
|
|
204
|
-
- Under
|
|
207
|
+
- Under any restricted policy, reject:
|
|
205
208
|
- `enableExternalAccess: true`
|
|
206
209
|
- `autoloadKnownExtensions: true`
|
|
207
210
|
- `autoinstallKnownExtensions: true`
|
|
@@ -210,26 +213,25 @@ Conflict checks:
|
|
|
210
213
|
- `motherDuckToken`
|
|
211
214
|
- remote or network-requiring `databasePath`
|
|
212
215
|
- Redundant matching values are allowed. For example,
|
|
213
|
-
`
|
|
216
|
+
`securityPolicy: "local"` plus `enableExternalAccess: false` is valid.
|
|
214
217
|
|
|
215
218
|
Derived defaults:
|
|
216
219
|
|
|
217
|
-
- Under
|
|
220
|
+
- Under any restricted policy, force:
|
|
218
221
|
- `enableExternalAccess = false`
|
|
222
|
+
- `lockConfiguration = true`
|
|
223
|
+
- `tempFileEncryption = true`
|
|
219
224
|
- `autoloadKnownExtensions = false`
|
|
220
225
|
- `autoinstallKnownExtensions = false`
|
|
221
226
|
- `allowCommunityExtensions = false`
|
|
222
227
|
- `allowUnsignedExtensions = false`
|
|
223
|
-
- Under
|
|
224
|
-
- `lockConfiguration = true`
|
|
225
|
-
- `tempFileEncryption = true`
|
|
226
|
-
- Under `filesystemPolicy: "sandboxed"`:
|
|
228
|
+
- Under `securityPolicy: "sandboxed"`:
|
|
227
229
|
- If `allowedDirectories` is omitted, derive it to exactly the canonical
|
|
228
230
|
`workingDirectory`, and nothing broader.
|
|
229
231
|
- If `tempDirectory` is omitted, derive it to `workingDirectory/.tmp`.
|
|
230
232
|
- If Malloy cannot derive the required paths safely, fail closed with a
|
|
231
233
|
field-specific error.
|
|
232
|
-
- Outside `
|
|
234
|
+
- Outside `securityPolicy: "sandboxed"`, do not invent an
|
|
233
235
|
`allowedDirectories` default.
|
|
234
236
|
- With `databasePath: ":memory:"`, normalize `readOnly` to `false`.
|
|
235
237
|
This intentionally preserves existing behavior. A user-facing warning for
|
|
@@ -278,8 +280,7 @@ effective setting that can affect runtime behavior, safety, or semantics:
|
|
|
278
280
|
|
|
279
281
|
- `databasePath`
|
|
280
282
|
- `readOnly`
|
|
281
|
-
- `
|
|
282
|
-
- `networkPolicy`
|
|
283
|
+
- `securityPolicy`
|
|
283
284
|
- `setupSQL`
|
|
284
285
|
- canonicalized `allowedDirectories`
|
|
285
286
|
- `enableExternalAccess`
|
|
@@ -346,7 +347,7 @@ Current baseline mapping:
|
|
|
346
347
|
- must be established before lock
|
|
347
348
|
- built-in extension loading
|
|
348
349
|
- preserve ordinary compatibility outside restricted modes
|
|
349
|
-
- under
|
|
350
|
+
- under any restricted policy, do not `INSTALL`, do not load `httpfs`,
|
|
350
351
|
and load only the fixed Malloy baseline extensions
|
|
351
352
|
- `setupSQL`
|
|
352
353
|
- preserve outside restricted modes
|
|
@@ -361,9 +362,9 @@ configuration after `lock_configuration=true`.
|
|
|
361
362
|
`icu` and `json` are part of the fixed Malloy DuckDB baseline.
|
|
362
363
|
|
|
363
364
|
`httpfs` is not part of that baseline. It broadens remote/network-capable
|
|
364
|
-
behavior and is controlled by `
|
|
365
|
+
behavior and is controlled by `securityPolicy`.
|
|
365
366
|
|
|
366
|
-
Under `
|
|
367
|
+
Under any restricted policy (`"local"` or `"sandboxed"`):
|
|
367
368
|
|
|
368
369
|
- do not load `httpfs`
|
|
369
370
|
- do not `INSTALL` extensions
|
|
@@ -371,7 +372,7 @@ Under `networkPolicy: "closed"`:
|
|
|
371
372
|
- load only fixed baseline extensions `icu` and `json`
|
|
372
373
|
- fail closed if a required baseline extension is unavailable locally
|
|
373
374
|
|
|
374
|
-
Outside
|
|
375
|
+
Outside restricted policies, preserve ordinary compatibility unless a
|
|
375
376
|
separate product decision changes it.
|
|
376
377
|
|
|
377
378
|
## Secrets
|
|
@@ -400,11 +401,10 @@ restricted-mode tests.
|
|
|
400
401
|
|
|
401
402
|
The policy system described here targets native DuckDB.
|
|
402
403
|
|
|
403
|
-
Do not register native-only policy
|
|
404
|
+
Do not register the native-only policy field in the `duckdb_wasm` connection
|
|
404
405
|
schema in this pass:
|
|
405
406
|
|
|
406
|
-
- `
|
|
407
|
-
- `networkPolicy`
|
|
407
|
+
- `securityPolicy`
|
|
408
408
|
- native-only hardening properties
|
|
409
409
|
|
|
410
410
|
Do not add native restricted-policy behavior to `DuckDBWASMConnection` as a
|
|
@@ -422,7 +422,7 @@ When adding a new native DuckDB config property, update all relevant layers:
|
|
|
422
422
|
|
|
423
423
|
- Register the property in `src/native.ts` with the correct config metadata.
|
|
424
424
|
- Parse and validate it in `normalizeDuckDBConfig()`.
|
|
425
|
-
- Decide whether it conflicts with `
|
|
425
|
+
- Decide whether it conflicts with `securityPolicy`.
|
|
426
426
|
- Decide whether any restricted policy must derive or force a value.
|
|
427
427
|
- If it is path-like, canonicalize it before validation and identity
|
|
428
428
|
comparison.
|
|
@@ -440,8 +440,8 @@ forced to a safe value under restricted policies.
|
|
|
440
440
|
|
|
441
441
|
## Changing Policy Behavior
|
|
442
442
|
|
|
443
|
-
When changing `
|
|
444
|
-
|
|
443
|
+
When changing `securityPolicy` or `NormalizedDuckDBSafetyPolicy`, review these
|
|
444
|
+
together:
|
|
445
445
|
|
|
446
446
|
- user-facing policy contract
|
|
447
447
|
- normalizer parsing and conflict checks
|
|
@@ -468,8 +468,7 @@ Keep focused tests for:
|
|
|
468
468
|
- `allowedDirectories` accepted as a JSON array of strings
|
|
469
469
|
- `allowedDirectories` rejected for non-array or non-string JSON values
|
|
470
470
|
- exact policy value parsing
|
|
471
|
-
- policy
|
|
472
|
-
values
|
|
471
|
+
- policy field rejected when provided as non-literal or reference-shaped value
|
|
473
472
|
- missing policy-required values failing closed
|
|
474
473
|
- conflict checks
|
|
475
474
|
- redundant matching explicit values accepted
|
|
@@ -478,10 +477,11 @@ Keep focused tests for:
|
|
|
478
477
|
- network-requiring `databasePath` rejection
|
|
479
478
|
- `readOnly: true` with `:memory:` normalizing to `false`
|
|
480
479
|
- share keys differing when safety-relevant settings differ
|
|
480
|
+
- different `securityPolicy` values producing different share keys
|
|
481
481
|
- semantically identical allowlists sharing
|
|
482
482
|
- restricted baseline order
|
|
483
|
-
-
|
|
484
|
-
-
|
|
483
|
+
- restricted policies not loading `httpfs`
|
|
484
|
+
- restricted policies not running `INSTALL`
|
|
485
485
|
- required baseline extensions loaded or failing closed
|
|
486
486
|
- later config-changing `SET` statements failing after lock
|
|
487
487
|
- `closeAllInstances()` clearing all share-keyed native instances
|
package/dist/duckdb_config.d.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import type { ConnectionConfig } from '@malloydata/malloy';
|
|
2
|
-
export type
|
|
3
|
-
export type DuckDBNetworkPolicy = 'open' | 'closed';
|
|
2
|
+
export type DuckDBSecurityPolicy = 'none' | 'local' | 'sandboxed';
|
|
4
3
|
export interface NormalizedDuckDBSafetyPolicy {
|
|
5
4
|
requiresPosixHost: boolean;
|
|
6
5
|
requiresLockedConfiguration: boolean;
|
|
@@ -9,7 +8,6 @@ export interface NormalizedDuckDBSafetyPolicy {
|
|
|
9
8
|
requiresTempFileEncryption: boolean;
|
|
10
9
|
requiresSecretNeutralization: boolean;
|
|
11
10
|
requiredBaselineExtensions: readonly ['icu', 'json'];
|
|
12
|
-
allowHttpfs: boolean;
|
|
13
11
|
forbidAdditionalExtensions: boolean;
|
|
14
12
|
derivedTempDirectoryName: '.tmp';
|
|
15
13
|
}
|
|
@@ -18,8 +16,7 @@ export interface NormalizedDuckDBConfig {
|
|
|
18
16
|
databasePath: string;
|
|
19
17
|
readOnly: boolean;
|
|
20
18
|
workingDirectory?: string;
|
|
21
|
-
|
|
22
|
-
networkPolicy: DuckDBNetworkPolicy;
|
|
19
|
+
securityPolicy: DuckDBSecurityPolicy;
|
|
23
20
|
safetyPolicy?: NormalizedDuckDBSafetyPolicy;
|
|
24
21
|
allowedDirectories?: string[];
|
|
25
22
|
enableExternalAccess?: boolean;
|
package/dist/duckdb_config.js
CHANGED
|
@@ -61,10 +61,9 @@ const DERIVED_TEMP_DIRECTORY_NAME = '.tmp';
|
|
|
61
61
|
const DERIVED_SECRET_DIRECTORY_NAME = '.duckdb-secrets';
|
|
62
62
|
function normalizeDuckDBConfig(config) {
|
|
63
63
|
var _a, _b, _c, _d;
|
|
64
|
-
const
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
throw new DuckDBConfigValidationError('filesystemPolicy "sandboxed" is only supported on POSIX hosts');
|
|
64
|
+
const securityPolicy = parseSecurityPolicy(config['securityPolicy']);
|
|
65
|
+
if (securityPolicy === 'sandboxed' && !pathSecurity.isPosixHost()) {
|
|
66
|
+
throw new DuckDBConfigValidationError('securityPolicy "sandboxed" is only supported on POSIX hosts');
|
|
68
67
|
}
|
|
69
68
|
const rawDatabasePath = (_b = (_a = readOptionalString(config, 'databasePath')) !== null && _a !== void 0 ? _a : readOptionalString(config, 'path')) !== null && _b !== void 0 ? _b : ':memory:';
|
|
70
69
|
const rawWorkingDirectory = readOptionalString(config, 'workingDirectory');
|
|
@@ -84,94 +83,92 @@ function normalizeDuckDBConfig(config) {
|
|
|
84
83
|
const rawTempDirectory = readOptionalString(config, 'tempDirectory');
|
|
85
84
|
const rawExtensionDirectory = readOptionalString(config, 'extensionDirectory');
|
|
86
85
|
const rawAllowedDirectories = readOptionalStringArray(config['allowedDirectories'], 'allowedDirectories');
|
|
87
|
-
const restricted =
|
|
86
|
+
const restricted = securityPolicy !== 'none';
|
|
88
87
|
const safetyPolicy = restricted
|
|
89
88
|
? {
|
|
90
|
-
requiresPosixHost:
|
|
89
|
+
requiresPosixHost: securityPolicy === 'sandboxed',
|
|
91
90
|
requiresLockedConfiguration: true,
|
|
92
91
|
requiresNoSetupSQL: true,
|
|
93
|
-
requiresSandboxedPaths:
|
|
92
|
+
requiresSandboxedPaths: securityPolicy === 'sandboxed',
|
|
94
93
|
requiresTempFileEncryption: true,
|
|
95
94
|
requiresSecretNeutralization: true,
|
|
96
95
|
requiredBaselineExtensions: REQUIRED_BASELINE_EXTENSIONS,
|
|
97
|
-
allowHttpfs: networkPolicy === 'open',
|
|
98
96
|
forbidAdditionalExtensions: true,
|
|
99
97
|
derivedTempDirectoryName: DERIVED_TEMP_DIRECTORY_NAME,
|
|
100
98
|
}
|
|
101
99
|
: undefined;
|
|
102
100
|
if ((safetyPolicy === null || safetyPolicy === void 0 ? void 0 : safetyPolicy.requiresNoSetupSQL) && rawSetupSQL !== undefined) {
|
|
103
|
-
throw new DuckDBConfigValidationError(
|
|
101
|
+
throw new DuckDBConfigValidationError(`setupSQL is not allowed when securityPolicy is "${securityPolicy}"`);
|
|
104
102
|
}
|
|
105
103
|
if ((safetyPolicy === null || safetyPolicy === void 0 ? void 0 : safetyPolicy.forbidAdditionalExtensions) &&
|
|
106
104
|
additionalExtensions.length > 0) {
|
|
107
|
-
throw new DuckDBConfigValidationError(
|
|
105
|
+
throw new DuckDBConfigValidationError(`additionalExtensions is not allowed when securityPolicy is "${securityPolicy}"`);
|
|
108
106
|
}
|
|
109
107
|
if (restricted && lockConfiguration === false) {
|
|
110
|
-
throw new DuckDBConfigValidationError(
|
|
108
|
+
throw new DuckDBConfigValidationError(`lockConfiguration cannot be false when securityPolicy is "${securityPolicy}"`);
|
|
111
109
|
}
|
|
112
110
|
if (restricted && tempFileEncryption === false) {
|
|
113
|
-
throw new DuckDBConfigValidationError(
|
|
114
|
-
}
|
|
115
|
-
if (
|
|
116
|
-
rejectConflictingBoolean(enableExternalAccess, 'enableExternalAccess', true,
|
|
117
|
-
rejectConflictingBoolean(autoloadKnownExtensions, 'autoloadKnownExtensions', true,
|
|
118
|
-
rejectConflictingBoolean(autoinstallKnownExtensions, 'autoinstallKnownExtensions', true,
|
|
119
|
-
rejectConflictingBoolean(allowCommunityExtensions, 'allowCommunityExtensions', true,
|
|
120
|
-
rejectConflictingBoolean(allowUnsignedExtensions, 'allowUnsignedExtensions', true,
|
|
111
|
+
throw new DuckDBConfigValidationError(`tempFileEncryption cannot be false when securityPolicy is "${securityPolicy}"`);
|
|
112
|
+
}
|
|
113
|
+
if (restricted) {
|
|
114
|
+
rejectConflictingBoolean(enableExternalAccess, 'enableExternalAccess', true, `securityPolicy "${securityPolicy}"`);
|
|
115
|
+
rejectConflictingBoolean(autoloadKnownExtensions, 'autoloadKnownExtensions', true, `securityPolicy "${securityPolicy}"`);
|
|
116
|
+
rejectConflictingBoolean(autoinstallKnownExtensions, 'autoinstallKnownExtensions', true, `securityPolicy "${securityPolicy}"`);
|
|
117
|
+
rejectConflictingBoolean(allowCommunityExtensions, 'allowCommunityExtensions', true, `securityPolicy "${securityPolicy}"`);
|
|
118
|
+
rejectConflictingBoolean(allowUnsignedExtensions, 'allowUnsignedExtensions', true, `securityPolicy "${securityPolicy}"`);
|
|
121
119
|
if (rawMotherDuckToken !== undefined) {
|
|
122
|
-
throw new DuckDBConfigValidationError(
|
|
120
|
+
throw new DuckDBConfigValidationError(`motherDuckToken is not allowed when securityPolicy is "${securityPolicy}"`);
|
|
123
121
|
}
|
|
124
122
|
}
|
|
125
123
|
let workingDirectory;
|
|
126
124
|
if (rawWorkingDirectory !== undefined) {
|
|
127
125
|
workingDirectory = canonicalizeConfigPath(rawWorkingDirectory, 'workingDirectory', {
|
|
128
|
-
mustExist:
|
|
126
|
+
mustExist: securityPolicy === 'sandboxed',
|
|
129
127
|
});
|
|
130
128
|
}
|
|
131
129
|
const databasePath = canonicalizeDatabasePath(rawDatabasePath);
|
|
132
130
|
const isMotherDuck = isMotherDuckPath(databasePath);
|
|
133
|
-
if (
|
|
134
|
-
|
|
135
|
-
throw new DuckDBConfigValidationError(`databasePath "${rawDatabasePath}" is not allowed when networkPolicy is "closed"`);
|
|
131
|
+
if (restricted && !isAllowedClosedNetworkDatabasePath(databasePath)) {
|
|
132
|
+
throw new DuckDBConfigValidationError(`databasePath "${rawDatabasePath}" is not allowed when securityPolicy is "${securityPolicy}"`);
|
|
136
133
|
}
|
|
137
|
-
if (
|
|
138
|
-
throw new DuckDBConfigValidationError(
|
|
134
|
+
if (restricted && isMotherDuck) {
|
|
135
|
+
throw new DuckDBConfigValidationError(`MotherDuck database paths are not allowed when securityPolicy is "${securityPolicy}"`);
|
|
139
136
|
}
|
|
140
137
|
const normalizedAllowedDirectories = rawAllowedDirectories === undefined
|
|
141
138
|
? undefined
|
|
142
139
|
: canonicalizeConfigPathList(rawAllowedDirectories, 'allowedDirectories');
|
|
143
140
|
let allowedDirectories = normalizedAllowedDirectories;
|
|
144
|
-
if (
|
|
141
|
+
if (securityPolicy === 'sandboxed') {
|
|
145
142
|
if (allowedDirectories === undefined) {
|
|
146
143
|
if (workingDirectory === undefined) {
|
|
147
|
-
throw new DuckDBConfigValidationError('
|
|
144
|
+
throw new DuckDBConfigValidationError('securityPolicy "sandboxed" requires either allowedDirectories or workingDirectory. If you expected workingDirectory to come from config.rootDirectory, verify that overlay is available.');
|
|
148
145
|
}
|
|
149
146
|
allowedDirectories = [workingDirectory];
|
|
150
147
|
}
|
|
151
148
|
if (workingDirectory !== undefined &&
|
|
152
149
|
!allowedDirectories.some(allowed => pathSecurity.isContainedPath(allowed, workingDirectory))) {
|
|
153
|
-
throw new DuckDBConfigValidationError('workingDirectory must be contained within allowedDirectories when
|
|
150
|
+
throw new DuckDBConfigValidationError('workingDirectory must be contained within allowedDirectories when securityPolicy is "sandboxed"');
|
|
154
151
|
}
|
|
155
152
|
}
|
|
156
153
|
let tempDirectory;
|
|
157
154
|
if (rawTempDirectory !== undefined) {
|
|
158
155
|
tempDirectory = canonicalizeConfigPath(rawTempDirectory, 'tempDirectory');
|
|
159
156
|
}
|
|
160
|
-
else if (
|
|
157
|
+
else if (securityPolicy === 'sandboxed') {
|
|
161
158
|
if (workingDirectory === undefined) {
|
|
162
|
-
throw new DuckDBConfigValidationError('
|
|
159
|
+
throw new DuckDBConfigValidationError('securityPolicy "sandboxed" requires tempDirectory or workingDirectory so Malloy can derive a safe temp directory');
|
|
163
160
|
}
|
|
164
161
|
tempDirectory = path_1.default.join(workingDirectory, DERIVED_TEMP_DIRECTORY_NAME);
|
|
165
162
|
}
|
|
166
|
-
if (
|
|
163
|
+
if (securityPolicy === 'sandboxed') {
|
|
167
164
|
if (allowedDirectories === undefined) {
|
|
168
|
-
throw new DuckDBConfigValidationError('
|
|
165
|
+
throw new DuckDBConfigValidationError('securityPolicy "sandboxed" requires allowedDirectories');
|
|
169
166
|
}
|
|
170
167
|
if (tempDirectory === undefined) {
|
|
171
|
-
throw new DuckDBConfigValidationError('
|
|
168
|
+
throw new DuckDBConfigValidationError('securityPolicy "sandboxed" requires tempDirectory');
|
|
172
169
|
}
|
|
173
170
|
if (!allowedDirectories.some(allowed => pathSecurity.isContainedPath(allowed, tempDirectory))) {
|
|
174
|
-
throw new DuckDBConfigValidationError('tempDirectory must be contained within allowedDirectories when
|
|
171
|
+
throw new DuckDBConfigValidationError('tempDirectory must be contained within allowedDirectories when securityPolicy is "sandboxed"');
|
|
175
172
|
}
|
|
176
173
|
}
|
|
177
174
|
const extensionDirectory = rawExtensionDirectory === undefined
|
|
@@ -187,18 +184,17 @@ function normalizeDuckDBConfig(config) {
|
|
|
187
184
|
databasePath,
|
|
188
185
|
readOnly: databasePath === ':memory:' ? false : readOnly,
|
|
189
186
|
workingDirectory,
|
|
190
|
-
|
|
191
|
-
networkPolicy,
|
|
187
|
+
securityPolicy,
|
|
192
188
|
safetyPolicy,
|
|
193
189
|
allowedDirectories,
|
|
194
|
-
enableExternalAccess:
|
|
190
|
+
enableExternalAccess: restricted ? false : enableExternalAccess,
|
|
195
191
|
lockConfiguration: (safetyPolicy === null || safetyPolicy === void 0 ? void 0 : safetyPolicy.requiresLockedConfiguration)
|
|
196
192
|
? true
|
|
197
193
|
: lockConfiguration,
|
|
198
|
-
autoloadKnownExtensions:
|
|
199
|
-
autoinstallKnownExtensions:
|
|
200
|
-
allowCommunityExtensions:
|
|
201
|
-
allowUnsignedExtensions:
|
|
194
|
+
autoloadKnownExtensions: restricted ? false : autoloadKnownExtensions,
|
|
195
|
+
autoinstallKnownExtensions: restricted ? false : autoinstallKnownExtensions,
|
|
196
|
+
allowCommunityExtensions: restricted ? false : allowCommunityExtensions,
|
|
197
|
+
allowUnsignedExtensions: restricted ? false : allowUnsignedExtensions,
|
|
202
198
|
tempFileEncryption: (safetyPolicy === null || safetyPolicy === void 0 ? void 0 : safetyPolicy.requiresTempFileEncryption)
|
|
203
199
|
? true
|
|
204
200
|
: tempFileEncryption,
|
|
@@ -216,7 +212,7 @@ function buildDuckDBShareKey(config) {
|
|
|
216
212
|
var _a, _b, _c, _d, _e, _f, _g;
|
|
217
213
|
// secretDirectory is derived from policy + workingDirectory/tempDirectory,
|
|
218
214
|
// which already participate in the share key.
|
|
219
|
-
return (0, malloy_1.makeDigest)('duckdb-share-key-
|
|
215
|
+
return (0, malloy_1.makeDigest)('duckdb-share-key-v2', config.databasePath, String(config.readOnly), config.securityPolicy, (_a = config.setupSQL) !== null && _a !== void 0 ? _a : '', ...((_b = config.allowedDirectories) !== null && _b !== void 0 ? _b : []), config.enableExternalAccess === undefined
|
|
220
216
|
? ''
|
|
221
217
|
: String(config.enableExternalAccess), config.lockConfiguration === undefined
|
|
222
218
|
? ''
|
|
@@ -241,21 +237,25 @@ function sqlStringListLiteral(values) {
|
|
|
241
237
|
function stringifyDuckDBOption(value) {
|
|
242
238
|
return String(value);
|
|
243
239
|
}
|
|
244
|
-
|
|
240
|
+
const SECURITY_POLICY_VALUES = [
|
|
241
|
+
'none',
|
|
242
|
+
'local',
|
|
243
|
+
'sandboxed',
|
|
244
|
+
];
|
|
245
|
+
function isSecurityPolicy(value) {
|
|
246
|
+
return SECURITY_POLICY_VALUES.includes(value);
|
|
247
|
+
}
|
|
248
|
+
function parseSecurityPolicy(rawValue) {
|
|
245
249
|
if (rawValue === undefined) {
|
|
246
|
-
return '
|
|
250
|
+
return 'none';
|
|
247
251
|
}
|
|
248
252
|
if (typeof rawValue !== 'string') {
|
|
249
|
-
throw new DuckDBConfigValidationError(
|
|
250
|
-
.map(v => `"${v}"`)
|
|
251
|
-
.join(' or ')}, got ${typeof rawValue}`);
|
|
253
|
+
throw new DuckDBConfigValidationError(`securityPolicy must be one of ${SECURITY_POLICY_VALUES.map(v => `"${v}"`).join(', ')}, got ${typeof rawValue}`);
|
|
252
254
|
}
|
|
253
|
-
if (
|
|
255
|
+
if (isSecurityPolicy(rawValue)) {
|
|
254
256
|
return rawValue;
|
|
255
257
|
}
|
|
256
|
-
throw new DuckDBConfigValidationError(
|
|
257
|
-
.map(v => `"${v}"`)
|
|
258
|
-
.join(' or ')}, got "${rawValue}"`);
|
|
258
|
+
throw new DuckDBConfigValidationError(`securityPolicy must be one of ${SECURITY_POLICY_VALUES.map(v => `"${v}"`).join(', ')}, got "${rawValue}"`);
|
|
259
259
|
}
|
|
260
260
|
function readOptionalString(config, fieldName) {
|
|
261
261
|
const rawValue = config[fieldName];
|
|
@@ -9,8 +9,7 @@ export interface DuckDBConnectionOptions extends ConnectionConfig {
|
|
|
9
9
|
workingDirectory?: string;
|
|
10
10
|
readOnly?: boolean;
|
|
11
11
|
setupSQL?: string;
|
|
12
|
-
|
|
13
|
-
networkPolicy?: 'open' | 'closed';
|
|
12
|
+
securityPolicy?: 'none' | 'local' | 'sandboxed';
|
|
14
13
|
allowedDirectories?: string[];
|
|
15
14
|
enableExternalAccess?: boolean;
|
|
16
15
|
lockConfiguration?: boolean;
|
|
@@ -50,7 +49,6 @@ export declare class DuckDBConnection extends DuckDBCommon {
|
|
|
50
49
|
private shouldApplyEnableExternalAccessAtOpenTime;
|
|
51
50
|
private applyFinalBaseline;
|
|
52
51
|
private loadBaselineExtensions;
|
|
53
|
-
private shouldLoadHttpfs;
|
|
54
52
|
private loadExtension;
|
|
55
53
|
protected runDuckDBQuery(sql: string): Promise<{
|
|
56
54
|
rows: QueryRecord[];
|
|
@@ -175,7 +175,7 @@ class DuckDBConnection extends duckdb_common_1.DuckDBCommon {
|
|
|
175
175
|
await this.loadBaselineExtensions();
|
|
176
176
|
}
|
|
177
177
|
async loadBaselineExtensions() {
|
|
178
|
-
if (this.normalized.
|
|
178
|
+
if (this.normalized.securityPolicy !== 'none') {
|
|
179
179
|
await this.loadExtension('json', { allowInstall: false, required: true });
|
|
180
180
|
await this.loadExtension('icu', { allowInstall: false, required: true });
|
|
181
181
|
return;
|
|
@@ -183,7 +183,7 @@ class DuckDBConnection extends duckdb_common_1.DuckDBCommon {
|
|
|
183
183
|
const allowInstall = this.normalized.enableExternalAccess !== false;
|
|
184
184
|
await this.loadExtension('json', { allowInstall, required: false });
|
|
185
185
|
await this.loadExtension('icu', { allowInstall, required: false });
|
|
186
|
-
if (this.
|
|
186
|
+
if (this.normalized.enableExternalAccess !== false) {
|
|
187
187
|
await this.loadExtension('httpfs', { allowInstall, required: false });
|
|
188
188
|
}
|
|
189
189
|
for (const extension of this.normalized.additionalExtensions) {
|
|
@@ -193,12 +193,6 @@ class DuckDBConnection extends duckdb_common_1.DuckDBCommon {
|
|
|
193
193
|
await this.loadExtension('motherduck', { allowInstall, required: false });
|
|
194
194
|
}
|
|
195
195
|
}
|
|
196
|
-
shouldLoadHttpfs() {
|
|
197
|
-
if (this.normalized.networkPolicy === 'closed') {
|
|
198
|
-
return false;
|
|
199
|
-
}
|
|
200
|
-
return this.normalized.enableExternalAccess !== false;
|
|
201
|
-
}
|
|
202
196
|
async loadExtension(extension, options) {
|
|
203
197
|
try {
|
|
204
198
|
await this.runDuckDBQuery(`LOAD ${(0, duckdb_config_1.sqlStringLiteral)(extension)}`);
|
package/dist/native.js
CHANGED
|
@@ -38,15 +38,8 @@ const duckdb_connection_1 = require("./duckdb_connection");
|
|
|
38
38
|
default: { config: 'rootDirectory' },
|
|
39
39
|
},
|
|
40
40
|
{
|
|
41
|
-
name: '
|
|
42
|
-
displayName: '
|
|
43
|
-
type: 'string',
|
|
44
|
-
optional: true,
|
|
45
|
-
requireLiteralString: true,
|
|
46
|
-
},
|
|
47
|
-
{
|
|
48
|
-
name: 'networkPolicy',
|
|
49
|
-
displayName: 'Network Policy',
|
|
41
|
+
name: 'securityPolicy',
|
|
42
|
+
displayName: 'Security Policy',
|
|
50
43
|
type: 'string',
|
|
51
44
|
optional: true,
|
|
52
45
|
requireLiteralString: true,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@malloydata/db-duckdb",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.380",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"types": "./dist/index.d.ts",
|
|
@@ -60,7 +60,7 @@
|
|
|
60
60
|
"dependencies": {
|
|
61
61
|
"@duckdb/duckdb-wasm": "1.33.1-dev13.0",
|
|
62
62
|
"@duckdb/node-api": "1.4.4-r.1",
|
|
63
|
-
"@malloydata/malloy": "0.0.
|
|
63
|
+
"@malloydata/malloy": "0.0.380",
|
|
64
64
|
"@motherduck/wasm-client": "^0.6.6",
|
|
65
65
|
"apache-arrow": "^17.0.0",
|
|
66
66
|
"web-worker": "^1.3.0"
|