@walkthru-earth/objex 1.1.0 → 1.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -1
- package/dist/components/browser/FileBrowser.svelte +25 -14
- package/dist/components/browser/FileTreeSidebar.svelte +43 -7
- package/dist/components/layout/ConnectionDialog.svelte +100 -1
- package/dist/components/layout/Sidebar.svelte +70 -25
- package/dist/components/viewers/ArchiveViewer.svelte +4 -4
- package/dist/components/viewers/CodeViewer.svelte +44 -5
- package/dist/components/viewers/CogControls.svelte +208 -0
- package/dist/components/viewers/CogControls.svelte.d.ts +12 -0
- package/dist/components/viewers/CogViewer.svelte +373 -1162
- package/dist/components/viewers/CogViewer.svelte.d.ts +1 -1
- package/dist/components/viewers/CopcViewer.svelte +20 -2
- package/dist/components/viewers/DatabaseViewer.svelte +345 -37
- package/dist/components/viewers/FlatGeobufViewer.svelte +15 -9
- package/dist/components/viewers/MarkdownViewer.svelte +1 -1
- package/dist/components/viewers/PmtilesViewer.svelte +2 -2
- package/dist/components/viewers/StacMapViewer.svelte +25 -9
- package/dist/components/viewers/TableViewer.svelte +162 -51
- package/dist/components/viewers/ZarrMapViewer.svelte +33 -4
- package/dist/components/viewers/ZarrViewer.svelte +3 -6
- package/dist/components/viewers/pmtiles/PmtilesMapView.svelte +0 -1
- package/dist/constants.d.ts +6 -2
- package/dist/constants.js +6 -2
- package/dist/file-icons/index.d.ts +1 -1
- package/dist/file-icons/index.js +12 -2
- package/dist/i18n/ar.js +25 -0
- package/dist/i18n/en.js +25 -0
- package/dist/i18n/index.svelte.d.ts +0 -1
- package/dist/i18n/index.svelte.js +0 -3
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/dist/query/engine.d.ts +20 -4
- package/dist/query/index.d.ts +2 -1
- package/dist/query/index.js +1 -0
- package/dist/query/source.d.ts +42 -0
- package/dist/query/source.js +54 -0
- package/dist/query/wasm.d.ts +7 -5
- package/dist/query/wasm.js +267 -107
- package/dist/storage/adapter.d.ts +9 -0
- package/dist/storage/adapter.js +13 -1
- package/dist/storage/browser-azure.d.ts +1 -1
- package/dist/storage/browser-azure.js +4 -0
- package/dist/storage/browser-cloud.d.ts +1 -1
- package/dist/storage/browser-cloud.js +7 -0
- package/dist/storage/presign.d.ts +13 -0
- package/dist/storage/presign.js +55 -0
- package/dist/storage/providers.d.ts +53 -0
- package/dist/storage/providers.js +171 -0
- package/dist/stores/browser.svelte.d.ts +2 -0
- package/dist/stores/browser.svelte.js +17 -1
- package/dist/stores/files.svelte.d.ts +1 -2
- package/dist/stores/files.svelte.js +1 -2
- package/dist/stores/tabs.svelte.d.ts +9 -2
- package/dist/stores/tabs.svelte.js +11 -2
- package/dist/types.d.ts +11 -0
- package/dist/utils/cog.d.ts +244 -0
- package/dist/utils/cog.js +1039 -0
- package/dist/utils/deck.d.ts +0 -18
- package/dist/utils/deck.js +0 -36
- package/dist/utils/geometry-type.d.ts +52 -0
- package/dist/utils/geometry-type.js +76 -0
- package/dist/utils/markdown-sql.d.ts +1 -1
- package/dist/utils/markdown-sql.js +3 -4
- package/dist/utils/pmtiles-tile.d.ts +0 -2
- package/dist/utils/pmtiles-tile.js +0 -8
- package/dist/utils/url-state.d.ts +6 -0
- package/dist/utils/url-state.js +34 -26
- package/dist/utils/url.d.ts +26 -9
- package/dist/utils/url.js +52 -25
- package/dist/utils/wkb.js +22 -8
- package/dist/utils/zarr-tab.d.ts +22 -0
- package/dist/utils/zarr-tab.js +30 -0
- package/dist/utils/zarr.d.ts +0 -2
- package/dist/utils/zarr.js +73 -44
- package/package.json +47 -43
- package/dist/components/ui/tabs/index.d.ts +0 -5
- package/dist/components/ui/tabs/index.js +0 -7
- package/dist/components/ui/tabs/tabs-content.svelte +0 -17
- package/dist/components/ui/tabs/tabs-content.svelte.d.ts +0 -4
- package/dist/components/ui/tabs/tabs-list.svelte +0 -16
- package/dist/components/ui/tabs/tabs-list.svelte.d.ts +0 -4
- package/dist/components/ui/tabs/tabs-trigger.svelte +0 -20
- package/dist/components/ui/tabs/tabs-trigger.svelte.d.ts +0 -4
- package/dist/components/ui/tabs/tabs.svelte +0 -19
- package/dist/components/ui/tabs/tabs.svelte.d.ts +0 -4
- package/dist/components/viewers/MapViewer.svelte +0 -234
- package/dist/components/viewers/MapViewer.svelte.d.ts +0 -7
- package/dist/components/viewers/StyleEditorOverlay.svelte +0 -27
- package/dist/components/viewers/StyleEditorOverlay.svelte.d.ts +0 -7
package/dist/storage/adapter.js
CHANGED
|
@@ -1 +1,13 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Thrown by adapters when the server returns 401 or 403 on an anonymous
|
|
3
|
+
* request. The browser store catches this to trigger a credential prompt
|
|
4
|
+
* for auto-detected `?url=` connections that turned out to be private.
|
|
5
|
+
*/
|
|
6
|
+
export class AuthRequiredError extends Error {
|
|
7
|
+
status;
|
|
8
|
+
constructor(status, message) {
|
|
9
|
+
super(message);
|
|
10
|
+
this.name = 'AuthRequiredError';
|
|
11
|
+
this.status = status;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { connectionStore } from '../stores/connections.svelte.js';
|
|
2
2
|
import { credentialStore } from '../stores/credentials.svelte.js';
|
|
3
|
+
import { AuthRequiredError } from './adapter.js';
|
|
3
4
|
// --- Helpers ---
|
|
4
5
|
function nameFromKey(key) {
|
|
5
6
|
const trimmed = key.replace(/\/$/, '');
|
|
@@ -92,6 +93,9 @@ export class BrowserAzureAdapter {
|
|
|
92
93
|
const res = await fetch(url, { signal });
|
|
93
94
|
if (!res.ok) {
|
|
94
95
|
const body = await res.text().catch(() => '');
|
|
96
|
+
if (res.status === 401 || res.status === 403) {
|
|
97
|
+
throw new AuthRequiredError(res.status, `Azure list failed (${res.status}): ${body || res.statusText}`);
|
|
98
|
+
}
|
|
95
99
|
throw new Error(`Azure list failed (${res.status}): ${body || res.statusText}`);
|
|
96
100
|
}
|
|
97
101
|
const xml = await res.text();
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { AwsClient } from 'aws4fetch';
|
|
2
2
|
import { connectionStore } from '../stores/connections.svelte.js';
|
|
3
3
|
import { credentialStore } from '../stores/credentials.svelte.js';
|
|
4
|
+
import { AuthRequiredError } from './adapter.js';
|
|
4
5
|
import { buildProviderBaseUrl, isGcsProvider } from './providers.js';
|
|
5
6
|
// --- Helpers ---
|
|
6
7
|
/** Extract the last path segment from an object key. */
|
|
@@ -128,6 +129,9 @@ export class BrowserCloudAdapter {
|
|
|
128
129
|
const res = await fetch(`${url}?${params}`, { signal });
|
|
129
130
|
if (!res.ok) {
|
|
130
131
|
const body = await res.text().catch(() => '');
|
|
132
|
+
if (res.status === 401 || res.status === 403) {
|
|
133
|
+
throw new AuthRequiredError(res.status, `GCS list failed (${res.status}): ${body || res.statusText}`);
|
|
134
|
+
}
|
|
131
135
|
throw new Error(`GCS list failed (${res.status}): ${body || res.statusText}`);
|
|
132
136
|
}
|
|
133
137
|
const json = await res.json();
|
|
@@ -185,6 +189,9 @@ export class BrowserCloudAdapter {
|
|
|
185
189
|
const res = await cloudFetch(`${baseUrl}?${params}`, { signal });
|
|
186
190
|
if (!res.ok) {
|
|
187
191
|
const body = await res.text().catch(() => '');
|
|
192
|
+
if (res.status === 401 || res.status === 403) {
|
|
193
|
+
throw new AuthRequiredError(res.status, `List failed (${res.status}): ${body || res.statusText}`);
|
|
194
|
+
}
|
|
188
195
|
throw new Error(`List failed (${res.status}): ${body || res.statusText}`);
|
|
189
196
|
}
|
|
190
197
|
const xml = await res.text();
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { Connection } from '../types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Presign an HTTPS URL using SigV4 query-string authentication (`X-Amz-*` params).
|
|
4
|
+
*
|
|
5
|
+
* Consumers like DuckDB's httpfs can fetch the returned URL directly with just a
|
|
6
|
+
* `Range` header, which avoids the `Authorization` header preflight that breaks
|
|
7
|
+
* on GCS's S3-compatible endpoint (cached preflight mismatches, `responseHeader`
|
|
8
|
+
* list not matching the browser's requested headers, etc.).
|
|
9
|
+
*
|
|
10
|
+
* Returns null when the connection is anonymous, Azure, or has no SigV4 creds.
|
|
11
|
+
* Callers should fall back to the `s3://` + SigV4 header path in that case.
|
|
12
|
+
*/
|
|
13
|
+
export declare function presignHttpsUrl(conn: Connection, key: string, expiresIn?: number): Promise<string | null>;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { credentialStore } from '../stores/credentials.svelte.js';
|
|
2
|
+
import { safeDecodeURIComponent } from '../utils/cloud-url.js';
|
|
3
|
+
import { buildProviderBaseUrl, getAccessMode } from './providers.js';
|
|
4
|
+
// 7 days is the SigV4 protocol maximum and is the hard cap on every
|
|
5
|
+
// S3-compatible provider we support (AWS, GCS, R2, B2, DO, Wasabi, Storj,
|
|
6
|
+
// Hetzner, Contabo, Linode, OVHcloud, MinIO). SDK defaults are lower
|
|
7
|
+
// (GCS ships 3600s) but that's a default, not a limit.
|
|
8
|
+
const MAX_EXPIRES_IN_SECONDS = 7 * 24 * 3600;
|
|
9
|
+
const DEFAULT_EXPIRES_IN_SECONDS = MAX_EXPIRES_IN_SECONDS;
|
|
10
|
+
/**
|
|
11
|
+
* Presign an HTTPS URL using SigV4 query-string authentication (`X-Amz-*` params).
|
|
12
|
+
*
|
|
13
|
+
* Consumers like DuckDB's httpfs can fetch the returned URL directly with just a
|
|
14
|
+
* `Range` header, which avoids the `Authorization` header preflight that breaks
|
|
15
|
+
* on GCS's S3-compatible endpoint (cached preflight mismatches, `responseHeader`
|
|
16
|
+
* list not matching the browser's requested headers, etc.).
|
|
17
|
+
*
|
|
18
|
+
* Returns null when the connection is anonymous, Azure, or has no SigV4 creds.
|
|
19
|
+
* Callers should fall back to the `s3://` + SigV4 header path in that case.
|
|
20
|
+
*/
|
|
21
|
+
export async function presignHttpsUrl(conn, key, expiresIn = DEFAULT_EXPIRES_IN_SECONDS) {
|
|
22
|
+
if (getAccessMode(conn) !== 'signed-s3')
|
|
23
|
+
return null;
|
|
24
|
+
const creds = credentialStore.get(conn.id);
|
|
25
|
+
if (!creds || creds.type !== 'sigv4')
|
|
26
|
+
return null;
|
|
27
|
+
const cleanKey = safeDecodeURIComponent(key.replace(/^\//, ''));
|
|
28
|
+
const baseUrl = buildProviderBaseUrl(conn.provider, conn.endpoint, conn.bucket, conn.region);
|
|
29
|
+
const url = new URL(`${baseUrl}/${encodeKey(cleanKey)}`);
|
|
30
|
+
// Clamp to the protocol max so callers asking for longer don't silently
|
|
31
|
+
// produce URLs every provider rejects.
|
|
32
|
+
const effectiveExpiry = Math.min(Math.max(1, expiresIn), MAX_EXPIRES_IN_SECONDS);
|
|
33
|
+
url.searchParams.set('X-Amz-Expires', String(effectiveExpiry));
|
|
34
|
+
// Lazy-load aws4fetch so public-only sessions don't pull it into the
|
|
35
|
+
// shared viewer chunk (utils/url.ts is imported widely).
|
|
36
|
+
const { AwsClient } = await import('aws4fetch');
|
|
37
|
+
const client = new AwsClient({
|
|
38
|
+
accessKeyId: creds.accessKey,
|
|
39
|
+
secretAccessKey: creds.secretKey,
|
|
40
|
+
service: 's3',
|
|
41
|
+
region: conn.region || 'us-east-1'
|
|
42
|
+
});
|
|
43
|
+
const signed = await client.sign(url.toString(), {
|
|
44
|
+
method: 'GET',
|
|
45
|
+
aws: { signQuery: true, allHeaders: false }
|
|
46
|
+
});
|
|
47
|
+
return signed.url;
|
|
48
|
+
}
|
|
49
|
+
/** Encode an object key for URL path, preserving `/` separators. */
|
|
50
|
+
function encodeKey(key) {
|
|
51
|
+
return key
|
|
52
|
+
.split('/')
|
|
53
|
+
.map((s) => encodeURIComponent(s))
|
|
54
|
+
.join('/');
|
|
55
|
+
}
|
|
@@ -38,12 +38,38 @@ export interface ProviderDef {
|
|
|
38
38
|
schemes: string[];
|
|
39
39
|
}
|
|
40
40
|
export declare const PROVIDERS: Record<ProviderId, ProviderDef>;
|
|
41
|
+
export interface CorsHelp {
|
|
42
|
+
/** True if the provider returns CORS headers by default. */
|
|
43
|
+
defaultEnabled: boolean;
|
|
44
|
+
/** Official CORS configuration docs URL. */
|
|
45
|
+
docsUrl?: string;
|
|
46
|
+
/** Brief note shown in the UI. */
|
|
47
|
+
note?: string;
|
|
48
|
+
/** CLI steps when no console UI or docs are insufficient. */
|
|
49
|
+
cliSteps?: string[];
|
|
50
|
+
}
|
|
51
|
+
export declare const CORS_HELP: Record<ProviderId, CorsHelp>;
|
|
52
|
+
export interface ReadOnlyHelp {
|
|
53
|
+
/** Brief note shown in the UI. */
|
|
54
|
+
note: string;
|
|
55
|
+
/** Official docs URL. */
|
|
56
|
+
docsUrl?: string;
|
|
57
|
+
/** CLI steps to apply a read-only bucket policy. */
|
|
58
|
+
cliSteps?: string[];
|
|
59
|
+
}
|
|
60
|
+
export declare const READ_ONLY_HELP: Partial<Record<ProviderId, ReadOnlyHelp>>;
|
|
41
61
|
/** All provider IDs, ordered for the UI. */
|
|
42
62
|
export declare const PROVIDER_IDS: ProviderId[];
|
|
43
63
|
/** Get provider def, falling back to S3 for unknown. */
|
|
44
64
|
export declare function getProvider(id: string): ProviderDef;
|
|
45
65
|
/** Build endpoint URL from template + region. */
|
|
46
66
|
export declare function buildEndpointFromTemplate(id: ProviderId, region: string): string;
|
|
67
|
+
/**
|
|
68
|
+
* Resolve an endpoint URL for a provider using its registered template,
|
|
69
|
+
* falling back to the provider's default region when none is supplied.
|
|
70
|
+
* Returns '' when the provider has no template (e.g. plain S3 or MinIO).
|
|
71
|
+
*/
|
|
72
|
+
export declare function resolveProviderEndpoint(provider: string, region?: string): string;
|
|
47
73
|
/**
|
|
48
74
|
* Build the base URL for API requests (endpoint + bucket).
|
|
49
75
|
* Used by browser-cloud adapter and url-state.
|
|
@@ -51,3 +77,30 @@ export declare function buildEndpointFromTemplate(id: ProviderId, region: string
|
|
|
51
77
|
export declare function buildProviderBaseUrl(provider: ProviderId, endpoint: string, bucket: string, region: string): string;
|
|
52
78
|
/** Check if a provider uses the GCS JSON API (not S3 XML). */
|
|
53
79
|
export declare function isGcsProvider(provider: string, endpoint: string): boolean;
|
|
80
|
+
/**
|
|
81
|
+
* Minimal connection shape needed to decide access mode.
|
|
82
|
+
* Kept loose so callers don't need to import the full Connection type.
|
|
83
|
+
*/
|
|
84
|
+
export interface AccessModeInput {
|
|
85
|
+
provider: string;
|
|
86
|
+
anonymous?: boolean;
|
|
87
|
+
endpoint?: string;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* How a connection's files can be read by the browser:
|
|
91
|
+
*
|
|
92
|
+
* - `public-https`: plain HTTPS via any HTTP client. No auth, no signing.
|
|
93
|
+
* Covers anonymous AWS/GCS/R2/Storj/Wasabi/etc.
|
|
94
|
+
* - `sas-https`: HTTPS with SAS token embedded in the URL. Still works with
|
|
95
|
+
* any HTTP client. Azure only.
|
|
96
|
+
* - `signed-s3`: requires SigV4 signing. DuckDB uses the `s3://` URI and
|
|
97
|
+
* signs it via its S3 config; other viewers must go through the storage
|
|
98
|
+
* adapter (which returns a blob) instead of streaming the HTTPS URL.
|
|
99
|
+
*/
|
|
100
|
+
export type AccessMode = 'public-https' | 'sas-https' | 'signed-s3';
|
|
101
|
+
export declare function getAccessMode(conn: AccessModeInput): AccessMode;
|
|
102
|
+
/**
|
|
103
|
+
* True when the connection's files can be fetched by any HTTP client
|
|
104
|
+
* (fetch/img/video/DuckDB httpfs/COG/Zarr/etc.) without the storage adapter.
|
|
105
|
+
*/
|
|
106
|
+
export declare function isPubliclyStreamable(conn: AccessModeInput): boolean;
|
|
@@ -266,6 +266,148 @@ export const PROVIDERS = {
|
|
|
266
266
|
schemes: []
|
|
267
267
|
}
|
|
268
268
|
};
|
|
269
|
+
export const CORS_HELP = {
|
|
270
|
+
s3: {
|
|
271
|
+
defaultEnabled: false,
|
|
272
|
+
docsUrl: 'https://docs.aws.amazon.com/AmazonS3/latest/userguide/enabling-cors-examples.html',
|
|
273
|
+
note: 'Enable via S3 Console: Bucket > Permissions > CORS, or use the AWS CLI.'
|
|
274
|
+
},
|
|
275
|
+
gcs: {
|
|
276
|
+
defaultEnabled: false,
|
|
277
|
+
docsUrl: 'https://cloud.google.com/storage/docs/using-cors',
|
|
278
|
+
note: 'Use the gcloud CLI. GCS `responseHeader` is dual-purpose (Access-Control-Expose-Headers AND Access-Control-Allow-Headers), so every request header the browser sends must be listed or the preflight fails silently. For private buckets signed with HMAC, include the AWS SigV4 headers (Authorization, x-amz-date, x-amz-content-sha256). For DuckDB httpfs partial reads, also include Range and the conditional If-* headers.',
|
|
279
|
+
cliSteps: [
|
|
280
|
+
'Create a cors.json file:\n[\n {\n "origin": ["*"],\n "method": ["GET", "HEAD"],\n "responseHeader": [\n "Content-Type",\n "Content-Length",\n "Content-Range",\n "Accept-Ranges",\n "Range",\n "If-Match",\n "If-Modified-Since",\n "If-None-Match",\n "If-Unmodified-Since",\n "ETag",\n "Authorization",\n "x-amz-content-sha256",\n "x-amz-date",\n "x-amz-*",\n "x-goog-*"\n ],\n "maxAgeSeconds": 3600\n }\n]',
|
|
281
|
+
'gcloud storage buckets update gs://BUCKET --cors-file=cors.json'
|
|
282
|
+
]
|
|
283
|
+
},
|
|
284
|
+
r2: {
|
|
285
|
+
defaultEnabled: false,
|
|
286
|
+
docsUrl: 'https://developers.cloudflare.com/r2/buckets/cors/',
|
|
287
|
+
note: 'Enable via R2 Dashboard: Bucket > Settings > CORS Policy.'
|
|
288
|
+
},
|
|
289
|
+
azure: {
|
|
290
|
+
defaultEnabled: false,
|
|
291
|
+
docsUrl: 'https://learn.microsoft.com/en-us/rest/api/storageservices/cross-origin-resource-sharing--cors--support-for-the-azure-storage-services',
|
|
292
|
+
note: 'Enable via Azure Portal: Storage Account > Blob Service > CORS, or use the Azure CLI.',
|
|
293
|
+
cliSteps: [
|
|
294
|
+
'az storage cors add --services b --methods GET HEAD \\\n --origins "*" --allowed-headers "*" \\\n --exposed-headers "*" --max-age 3600 \\\n --account-name ACCOUNT'
|
|
295
|
+
]
|
|
296
|
+
},
|
|
297
|
+
minio: {
|
|
298
|
+
defaultEnabled: true,
|
|
299
|
+
docsUrl: 'https://docs.min.io/enterprise/aistor-object-store/reference/cli/mc-cors/',
|
|
300
|
+
note: 'MinIO allows all origins by default. For custom rules, use mc cors set.'
|
|
301
|
+
},
|
|
302
|
+
storj: {
|
|
303
|
+
defaultEnabled: true,
|
|
304
|
+
note: 'Storj S3 gateway returns CORS headers by default.'
|
|
305
|
+
},
|
|
306
|
+
b2: {
|
|
307
|
+
defaultEnabled: false,
|
|
308
|
+
docsUrl: 'https://www.backblaze.com/docs/cloud-storage-cross-origin-resource-sharing-rules',
|
|
309
|
+
note: 'Enable via B2 Console: Bucket Settings > CORS Rules, or use the B2 CLI.',
|
|
310
|
+
cliSteps: [
|
|
311
|
+
'b2 bucket update --cors-rules \'[{\n "corsRuleName": "allow-all",\n "allowedOrigins": ["*"],\n "allowedOperations": ["s3_head", "s3_get"],\n "allowedHeaders": ["*"],\n "maxAgeSeconds": 3600\n}]\' BUCKET allPublic'
|
|
312
|
+
]
|
|
313
|
+
},
|
|
314
|
+
digitalocean: {
|
|
315
|
+
defaultEnabled: false,
|
|
316
|
+
docsUrl: 'https://docs.digitalocean.com/products/spaces/how-to/configure-cors/',
|
|
317
|
+
note: 'Enable via Control Panel: Space > Settings > CORS Configurations.'
|
|
318
|
+
},
|
|
319
|
+
wasabi: {
|
|
320
|
+
defaultEnabled: true,
|
|
321
|
+
docsUrl: 'https://docs.wasabi.com/docs/bucket-policy',
|
|
322
|
+
note: 'Wasabi returns CORS headers by default for all buckets.'
|
|
323
|
+
},
|
|
324
|
+
contabo: {
|
|
325
|
+
defaultEnabled: false,
|
|
326
|
+
note: 'S3-compatible CORS via the AWS CLI.',
|
|
327
|
+
cliSteps: [
|
|
328
|
+
'Create a cors.json file:\n{\n "CORSRules": [{\n "AllowedOrigins": ["*"],\n "AllowedMethods": ["GET", "HEAD"],\n "AllowedHeaders": ["*"],\n "ExposeHeaders": ["ETag", "Content-Length", "Content-Type", "Content-Range", "Accept-Ranges"],\n "MaxAgeSeconds": 3600\n }]\n}',
|
|
329
|
+
'aws s3api put-bucket-cors --bucket BUCKET \\\n --cors-configuration file://cors.json \\\n --endpoint-url https://REGION.contaboobj.com'
|
|
330
|
+
]
|
|
331
|
+
},
|
|
332
|
+
hetzner: {
|
|
333
|
+
defaultEnabled: false,
|
|
334
|
+
docsUrl: 'https://docs.hetzner.com/storage/object-storage/howto-protect-objects/cors/',
|
|
335
|
+
note: 'S3-compatible CORS via the AWS CLI.',
|
|
336
|
+
cliSteps: [
|
|
337
|
+
'Create a cors.json file:\n{\n "CORSRules": [{\n "AllowedOrigins": ["*"],\n "AllowedMethods": ["GET", "HEAD"],\n "AllowedHeaders": ["*"],\n "ExposeHeaders": ["ETag", "Content-Length", "Content-Type", "Content-Range", "Accept-Ranges"],\n "MaxAgeSeconds": 3600\n }]\n}',
|
|
338
|
+
'aws s3api put-bucket-cors --bucket BUCKET \\\n --cors-configuration file://cors.json \\\n --endpoint-url https://REGION.your-objectstorage.com \\\n --region REGION'
|
|
339
|
+
]
|
|
340
|
+
},
|
|
341
|
+
linode: {
|
|
342
|
+
defaultEnabled: false,
|
|
343
|
+
docsUrl: 'https://www.linode.com/docs/guides/working-with-cors-linode-object-storage/',
|
|
344
|
+
note: 'S3-compatible CORS via the AWS CLI.',
|
|
345
|
+
cliSteps: [
|
|
346
|
+
'Create a cors.json file:\n{\n "CORSRules": [{\n "AllowedOrigins": ["*"],\n "AllowedMethods": ["GET", "HEAD"],\n "AllowedHeaders": ["*"],\n "ExposeHeaders": ["ETag", "Content-Length", "Content-Type", "Content-Range", "Accept-Ranges"],\n "MaxAgeSeconds": 3600\n }]\n}',
|
|
347
|
+
'aws s3api put-bucket-cors --bucket BUCKET \\\n --cors-configuration file://cors.json \\\n --endpoint-url https://REGION.linodeobjects.com'
|
|
348
|
+
]
|
|
349
|
+
},
|
|
350
|
+
ovhcloud: {
|
|
351
|
+
defaultEnabled: false,
|
|
352
|
+
docsUrl: 'https://help.ovhcloud.com/csm/en-public-cloud-storage-s3-cors?id=kb_article_view&sysparm_article=KB0058291',
|
|
353
|
+
note: 'S3-compatible CORS via the AWS CLI.',
|
|
354
|
+
cliSteps: [
|
|
355
|
+
'Create a cors.json file:\n{\n "CORSRules": [{\n "AllowedOrigins": ["*"],\n "AllowedMethods": ["GET", "HEAD"],\n "AllowedHeaders": ["*"],\n "ExposeHeaders": ["ETag", "Content-Length", "Content-Type", "Content-Range", "Accept-Ranges"],\n "MaxAgeSeconds": 3600\n }]\n}',
|
|
356
|
+
'aws s3api put-bucket-cors --bucket BUCKET \\\n --cors-configuration file://cors.json \\\n --endpoint-url https://s3.REGION.io.cloud.ovh.net'
|
|
357
|
+
]
|
|
358
|
+
}
|
|
359
|
+
};
|
|
360
|
+
export const READ_ONLY_HELP = {
|
|
361
|
+
s3: {
|
|
362
|
+
note: 'Use IAM policies to create a read-only user, or apply a bucket policy that allows only s3:GetObject and s3:ListBucket.',
|
|
363
|
+
docsUrl: 'https://docs.aws.amazon.com/AmazonS3/latest/userguide/example-policies-s3.html'
|
|
364
|
+
},
|
|
365
|
+
gcs: {
|
|
366
|
+
note: 'Assign the Storage Object Viewer role (roles/storage.objectViewer) to the service account.',
|
|
367
|
+
docsUrl: 'https://cloud.google.com/storage/docs/access-control/iam-roles'
|
|
368
|
+
},
|
|
369
|
+
r2: {
|
|
370
|
+
note: 'Create an API token with Object Read permissions in the R2 dashboard.',
|
|
371
|
+
docsUrl: 'https://developers.cloudflare.com/r2/api/tokens/'
|
|
372
|
+
},
|
|
373
|
+
azure: {
|
|
374
|
+
note: 'Generate a SAS token with Read and List permissions only. Avoid granting Write or Delete.',
|
|
375
|
+
docsUrl: 'https://learn.microsoft.com/en-us/azure/storage/common/storage-sas-overview'
|
|
376
|
+
},
|
|
377
|
+
b2: {
|
|
378
|
+
note: 'Create an application key with readFiles and listBuckets capabilities only.',
|
|
379
|
+
docsUrl: 'https://www.backblaze.com/docs/cloud-storage-application-keys'
|
|
380
|
+
},
|
|
381
|
+
hetzner: {
|
|
382
|
+
note: 'Keys have full read/write by default. Use a bucket policy with the correct ARN format to deny write and policy actions. To undo, generate a new admin key from the Hetzner Console.',
|
|
383
|
+
docsUrl: 'https://docs.hetzner.com/storage/object-storage/faq/s3-credentials/#how-do-i-restrict-access-per-key',
|
|
384
|
+
cliSteps: [
|
|
385
|
+
'Find your project ID from the Hetzner Console URL:\nhttps://console.hetzner.com/projects/<PROJECT_ID>/servers',
|
|
386
|
+
'Create a policy.json file:\n{\n "Version": "2012-10-17",\n "Statement": [\n {\n "Sid": "DenyWrites",\n "Effect": "Deny",\n "Principal": {\n "AWS": "arn:aws:iam:::user/p<PROJECT_ID>:<ACCESS_KEY>"\n },\n "Action": [\n "s3:PutObject",\n "s3:DeleteObject",\n "s3:AbortMultipartUpload",\n "s3:PutBucketPolicy",\n "s3:DeleteBucketPolicy"\n ],\n "Resource": [\n "arn:aws:s3:::BUCKET",\n "arn:aws:s3:::BUCKET/*"\n ]\n }\n ]\n}',
|
|
387
|
+
'aws s3api put-bucket-policy --bucket BUCKET \\\n --policy file://policy.json \\\n --endpoint-url https://REGION.your-objectstorage.com \\\n --region REGION',
|
|
388
|
+
'Note: This key can no longer modify the policy.\nTo restore write access, generate a new key in the\nHetzner Console and use it to delete the policy.'
|
|
389
|
+
]
|
|
390
|
+
},
|
|
391
|
+
minio: {
|
|
392
|
+
note: 'Create a read-only policy with mc admin policy, or use the built-in readonly canned policy.',
|
|
393
|
+
docsUrl: 'https://docs.min.io/enterprise/aistor-object-store/administration/iam/access/'
|
|
394
|
+
},
|
|
395
|
+
digitalocean: {
|
|
396
|
+
note: 'Spaces keys are project-wide. Use a bucket policy to restrict write actions for a specific key.',
|
|
397
|
+
docsUrl: 'https://docs.digitalocean.com/products/spaces/how-to/manage-access/'
|
|
398
|
+
},
|
|
399
|
+
wasabi: {
|
|
400
|
+
note: 'Create a sub-user with a read-only policy in the Wasabi Console.',
|
|
401
|
+
docsUrl: 'https://docs.wasabi.com/docs/creating-a-user-account-and-access-key'
|
|
402
|
+
},
|
|
403
|
+
contabo: {
|
|
404
|
+
note: 'S3-compatible bucket policies. Use a Deny policy for write actions with the key ARN.',
|
|
405
|
+
cliSteps: [
|
|
406
|
+
'Create a policy.json with a Deny statement for s3:PutObject and s3:DeleteObject.',
|
|
407
|
+
'aws s3api put-bucket-policy --bucket BUCKET \\\n --policy file://policy.json \\\n --endpoint-url https://REGION.contaboobj.com'
|
|
408
|
+
]
|
|
409
|
+
}
|
|
410
|
+
};
|
|
269
411
|
// ---------------------------------------------------------------------------
|
|
270
412
|
// Helpers
|
|
271
413
|
// ---------------------------------------------------------------------------
|
|
@@ -296,6 +438,17 @@ export function buildEndpointFromTemplate(id, region) {
|
|
|
296
438
|
return '';
|
|
297
439
|
return def.endpointTemplate.replace('{region}', region);
|
|
298
440
|
}
|
|
441
|
+
/**
|
|
442
|
+
* Resolve an endpoint URL for a provider using its registered template,
|
|
443
|
+
* falling back to the provider's default region when none is supplied.
|
|
444
|
+
* Returns '' when the provider has no template (e.g. plain S3 or MinIO).
|
|
445
|
+
*/
|
|
446
|
+
export function resolveProviderEndpoint(provider, region) {
|
|
447
|
+
const def = PROVIDERS[provider];
|
|
448
|
+
if (!def?.endpointTemplate)
|
|
449
|
+
return '';
|
|
450
|
+
return buildEndpointFromTemplate(provider, region || def.defaultRegion);
|
|
451
|
+
}
|
|
299
452
|
/**
|
|
300
453
|
* Build the base URL for API requests (endpoint + bucket).
|
|
301
454
|
* Used by browser-cloud adapter and url-state.
|
|
@@ -316,3 +469,21 @@ export function buildProviderBaseUrl(provider, endpoint, bucket, region) {
|
|
|
316
469
|
export function isGcsProvider(provider, endpoint) {
|
|
317
470
|
return provider === 'gcs' || (!!endpoint && /storage\.googleapis\.com/i.test(endpoint));
|
|
318
471
|
}
|
|
472
|
+
export function getAccessMode(conn) {
|
|
473
|
+
if (conn.provider === 'azure')
|
|
474
|
+
return 'sas-https';
|
|
475
|
+
// Anonymous buckets: every provider serves files over plain HTTPS without
|
|
476
|
+
// signing (AWS path/vhost, GCS, R2 public, Storj, Wasabi, DO, etc.).
|
|
477
|
+
if (conn.anonymous)
|
|
478
|
+
return 'public-https';
|
|
479
|
+
// Authenticated: needs SigV4 signing.
|
|
480
|
+
return 'signed-s3';
|
|
481
|
+
}
|
|
482
|
+
/**
|
|
483
|
+
* True when the connection's files can be fetched by any HTTP client
|
|
484
|
+
* (fetch/img/video/DuckDB httpfs/COG/Zarr/etc.) without the storage adapter.
|
|
485
|
+
*/
|
|
486
|
+
export function isPubliclyStreamable(conn) {
|
|
487
|
+
const mode = getAccessMode(conn);
|
|
488
|
+
return mode === 'public-https' || mode === 'sas-https';
|
|
489
|
+
}
|
|
@@ -11,6 +11,8 @@ export declare const browser: {
|
|
|
11
11
|
total: number;
|
|
12
12
|
};
|
|
13
13
|
readonly canWrite: boolean;
|
|
14
|
+
readonly authRequired: Connection | null;
|
|
15
|
+
clearAuthRequired: () => void;
|
|
14
16
|
browse: (connection: Connection, prefix?: string) => Promise<void>;
|
|
15
17
|
navigateTo: (prefix: string) => Promise<void>;
|
|
16
18
|
navigateUp: () => Promise<void>;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { AuthRequiredError } from '../storage/adapter.js';
|
|
1
2
|
import { getAdapter } from '../storage/index.js';
|
|
2
3
|
import { credentialStore } from './credentials.svelte.js';
|
|
3
4
|
import { safeLock } from './safelock.svelte.js';
|
|
@@ -7,6 +8,7 @@ function createBrowserStore() {
|
|
|
7
8
|
let entries = $state([]);
|
|
8
9
|
let loading = $state(false);
|
|
9
10
|
let error = $state(null);
|
|
11
|
+
let authRequired = $state(null);
|
|
10
12
|
let uploading = $state(false);
|
|
11
13
|
let uploadProgress = $state({ current: 0, total: 0 });
|
|
12
14
|
async function browse(connection, prefix) {
|
|
@@ -22,12 +24,22 @@ function createBrowserStore() {
|
|
|
22
24
|
entries = result;
|
|
23
25
|
}
|
|
24
26
|
catch (e) {
|
|
25
|
-
|
|
27
|
+
if (e instanceof AuthRequiredError && connection.anonymous) {
|
|
28
|
+
// Auto-detected a private bucket. Surface it for the Sidebar to
|
|
29
|
+
// flip the connection to non-anonymous and prompt for credentials.
|
|
30
|
+
authRequired = connection;
|
|
31
|
+
}
|
|
32
|
+
else {
|
|
33
|
+
error = e instanceof Error ? e.message : String(e);
|
|
34
|
+
}
|
|
26
35
|
}
|
|
27
36
|
finally {
|
|
28
37
|
loading = false;
|
|
29
38
|
}
|
|
30
39
|
}
|
|
40
|
+
function clearAuthRequired() {
|
|
41
|
+
authRequired = null;
|
|
42
|
+
}
|
|
31
43
|
async function navigateTo(prefix) {
|
|
32
44
|
if (!activeConnection)
|
|
33
45
|
return;
|
|
@@ -145,6 +157,10 @@ function createBrowserStore() {
|
|
|
145
157
|
return false;
|
|
146
158
|
return credentialStore.has(activeConnection.id);
|
|
147
159
|
},
|
|
160
|
+
get authRequired() {
|
|
161
|
+
return authRequired;
|
|
162
|
+
},
|
|
163
|
+
clearAuthRequired,
|
|
148
164
|
browse,
|
|
149
165
|
navigateTo,
|
|
150
166
|
navigateUp,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { FileEntry } from '../types.js';
|
|
2
2
|
import { type SortConfig, type SortField } from '../utils/file-sort.js';
|
|
3
|
-
export declare const
|
|
3
|
+
export declare const files: {
|
|
4
4
|
readonly entries: FileEntry[];
|
|
5
5
|
readonly currentPath: string;
|
|
6
6
|
readonly loading: boolean;
|
|
@@ -12,4 +12,3 @@ export declare const fileStore: {
|
|
|
12
12
|
setError(message: string | null): void;
|
|
13
13
|
sort(field: SortField): void;
|
|
14
14
|
};
|
|
15
|
-
export { fileStore as files };
|
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
import type { Tab } from '../types.js';
|
|
2
|
-
|
|
2
|
+
/**
|
|
3
|
+
* Tab-id for eagerly-opened direct-URL tabs (`source: 'url'`).
|
|
4
|
+
* The Sidebar's host-detection auto-migration closes an eager tab by its id
|
|
5
|
+
* and re-opens as a remote tab once a connection is available; the eager id
|
|
6
|
+
* is built in `+page.svelte::openUrlTab` and matched in `Sidebar.svelte::
|
|
7
|
+
* handleAutoDetection`, so both sides must agree on the format.
|
|
8
|
+
*/
|
|
9
|
+
export declare function eagerUrlTabId(url: string): string;
|
|
10
|
+
export declare const tabs: {
|
|
3
11
|
readonly items: Tab[];
|
|
4
12
|
readonly activeTabId: string | null;
|
|
5
13
|
readonly active: Tab | undefined;
|
|
@@ -14,4 +22,3 @@ export declare const tabStore: {
|
|
|
14
22
|
setActive(id: string): void;
|
|
15
23
|
update(id: string, partial: Partial<Omit<Tab, "id">>): void;
|
|
16
24
|
};
|
|
17
|
-
export { tabStore as tabs };
|
|
@@ -1,6 +1,16 @@
|
|
|
1
1
|
import { tabResources } from './tab-resources.svelte.js';
|
|
2
2
|
/** Maximum number of viewer instances kept alive (mounted but hidden). */
|
|
3
3
|
const MAX_ALIVE = 5;
|
|
4
|
+
/**
|
|
5
|
+
* Tab-id for eagerly-opened direct-URL tabs (`source: 'url'`).
|
|
6
|
+
* The Sidebar's host-detection auto-migration closes an eager tab by its id
|
|
7
|
+
* and re-opens as a remote tab once a connection is available; the eager id
|
|
8
|
+
* is built in `+page.svelte::openUrlTab` and matched in `Sidebar.svelte::
|
|
9
|
+
* handleAutoDetection`, so both sides must agree on the format.
|
|
10
|
+
*/
|
|
11
|
+
export function eagerUrlTabId(url) {
|
|
12
|
+
return `url:${url}`;
|
|
13
|
+
}
|
|
4
14
|
function releaseDuckDbMemory() {
|
|
5
15
|
import('../query/index.js')
|
|
6
16
|
.then(({ getQueryEngine }) => getQueryEngine().then((engine) => engine.releaseMemory()))
|
|
@@ -106,5 +116,4 @@ function createTabsStore() {
|
|
|
106
116
|
}
|
|
107
117
|
};
|
|
108
118
|
}
|
|
109
|
-
export const
|
|
110
|
-
export { tabStore as tabs };
|
|
119
|
+
export const tabs = createTabsStore();
|
package/dist/types.d.ts
CHANGED
|
@@ -38,6 +38,17 @@ export interface Tab {
|
|
|
38
38
|
connectionId?: string;
|
|
39
39
|
extension: string;
|
|
40
40
|
size?: number;
|
|
41
|
+
/**
|
|
42
|
+
* When set, the tab reads data from a SQL FROM-clause target (e.g. an
|
|
43
|
+
* attached DuckLake/DuckDB/SQLite table) rather than a file URL. The ref
|
|
44
|
+
* is inserted directly into generated SQL, so it must be fully-qualified
|
|
45
|
+
* and pre-quoted, e.g. `__objex_db__."main"."air_quality"`.
|
|
46
|
+
*
|
|
47
|
+
* When `sourceRef` is set, file-specific loading paths (hyparquet
|
|
48
|
+
* metadata, `parquet_kv_metadata`, etc.) are skipped, and schema / CRS /
|
|
49
|
+
* row count are derived from the SQL source directly via DuckDB.
|
|
50
|
+
*/
|
|
51
|
+
sourceRef?: string;
|
|
41
52
|
}
|
|
42
53
|
export interface WriteResult {
|
|
43
54
|
key: string;
|