@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.
Files changed (89) hide show
  1. package/README.md +3 -1
  2. package/dist/components/browser/FileBrowser.svelte +25 -14
  3. package/dist/components/browser/FileTreeSidebar.svelte +43 -7
  4. package/dist/components/layout/ConnectionDialog.svelte +100 -1
  5. package/dist/components/layout/Sidebar.svelte +70 -25
  6. package/dist/components/viewers/ArchiveViewer.svelte +4 -4
  7. package/dist/components/viewers/CodeViewer.svelte +44 -5
  8. package/dist/components/viewers/CogControls.svelte +208 -0
  9. package/dist/components/viewers/CogControls.svelte.d.ts +12 -0
  10. package/dist/components/viewers/CogViewer.svelte +373 -1162
  11. package/dist/components/viewers/CogViewer.svelte.d.ts +1 -1
  12. package/dist/components/viewers/CopcViewer.svelte +20 -2
  13. package/dist/components/viewers/DatabaseViewer.svelte +345 -37
  14. package/dist/components/viewers/FlatGeobufViewer.svelte +15 -9
  15. package/dist/components/viewers/MarkdownViewer.svelte +1 -1
  16. package/dist/components/viewers/PmtilesViewer.svelte +2 -2
  17. package/dist/components/viewers/StacMapViewer.svelte +25 -9
  18. package/dist/components/viewers/TableViewer.svelte +162 -51
  19. package/dist/components/viewers/ZarrMapViewer.svelte +33 -4
  20. package/dist/components/viewers/ZarrViewer.svelte +3 -6
  21. package/dist/components/viewers/pmtiles/PmtilesMapView.svelte +0 -1
  22. package/dist/constants.d.ts +6 -2
  23. package/dist/constants.js +6 -2
  24. package/dist/file-icons/index.d.ts +1 -1
  25. package/dist/file-icons/index.js +12 -2
  26. package/dist/i18n/ar.js +25 -0
  27. package/dist/i18n/en.js +25 -0
  28. package/dist/i18n/index.svelte.d.ts +0 -1
  29. package/dist/i18n/index.svelte.js +0 -3
  30. package/dist/index.d.ts +2 -0
  31. package/dist/index.js +1 -0
  32. package/dist/query/engine.d.ts +20 -4
  33. package/dist/query/index.d.ts +2 -1
  34. package/dist/query/index.js +1 -0
  35. package/dist/query/source.d.ts +42 -0
  36. package/dist/query/source.js +54 -0
  37. package/dist/query/wasm.d.ts +7 -5
  38. package/dist/query/wasm.js +267 -107
  39. package/dist/storage/adapter.d.ts +9 -0
  40. package/dist/storage/adapter.js +13 -1
  41. package/dist/storage/browser-azure.d.ts +1 -1
  42. package/dist/storage/browser-azure.js +4 -0
  43. package/dist/storage/browser-cloud.d.ts +1 -1
  44. package/dist/storage/browser-cloud.js +7 -0
  45. package/dist/storage/presign.d.ts +13 -0
  46. package/dist/storage/presign.js +55 -0
  47. package/dist/storage/providers.d.ts +53 -0
  48. package/dist/storage/providers.js +171 -0
  49. package/dist/stores/browser.svelte.d.ts +2 -0
  50. package/dist/stores/browser.svelte.js +17 -1
  51. package/dist/stores/files.svelte.d.ts +1 -2
  52. package/dist/stores/files.svelte.js +1 -2
  53. package/dist/stores/tabs.svelte.d.ts +9 -2
  54. package/dist/stores/tabs.svelte.js +11 -2
  55. package/dist/types.d.ts +11 -0
  56. package/dist/utils/cog.d.ts +244 -0
  57. package/dist/utils/cog.js +1039 -0
  58. package/dist/utils/deck.d.ts +0 -18
  59. package/dist/utils/deck.js +0 -36
  60. package/dist/utils/geometry-type.d.ts +52 -0
  61. package/dist/utils/geometry-type.js +76 -0
  62. package/dist/utils/markdown-sql.d.ts +1 -1
  63. package/dist/utils/markdown-sql.js +3 -4
  64. package/dist/utils/pmtiles-tile.d.ts +0 -2
  65. package/dist/utils/pmtiles-tile.js +0 -8
  66. package/dist/utils/url-state.d.ts +6 -0
  67. package/dist/utils/url-state.js +34 -26
  68. package/dist/utils/url.d.ts +26 -9
  69. package/dist/utils/url.js +52 -25
  70. package/dist/utils/wkb.js +22 -8
  71. package/dist/utils/zarr-tab.d.ts +22 -0
  72. package/dist/utils/zarr-tab.js +30 -0
  73. package/dist/utils/zarr.d.ts +0 -2
  74. package/dist/utils/zarr.js +73 -44
  75. package/package.json +47 -43
  76. package/dist/components/ui/tabs/index.d.ts +0 -5
  77. package/dist/components/ui/tabs/index.js +0 -7
  78. package/dist/components/ui/tabs/tabs-content.svelte +0 -17
  79. package/dist/components/ui/tabs/tabs-content.svelte.d.ts +0 -4
  80. package/dist/components/ui/tabs/tabs-list.svelte +0 -16
  81. package/dist/components/ui/tabs/tabs-list.svelte.d.ts +0 -4
  82. package/dist/components/ui/tabs/tabs-trigger.svelte +0 -20
  83. package/dist/components/ui/tabs/tabs-trigger.svelte.d.ts +0 -4
  84. package/dist/components/ui/tabs/tabs.svelte +0 -19
  85. package/dist/components/ui/tabs/tabs.svelte.d.ts +0 -4
  86. package/dist/components/viewers/MapViewer.svelte +0 -234
  87. package/dist/components/viewers/MapViewer.svelte.d.ts +0 -7
  88. package/dist/components/viewers/StyleEditorOverlay.svelte +0 -27
  89. package/dist/components/viewers/StyleEditorOverlay.svelte.d.ts +0 -7
@@ -1 +1,13 @@
1
- export {};
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,5 @@
1
1
  import type { FileEntry, WriteResult } from '../types.js';
2
- import type { ListPage, StorageAdapter } from './adapter.js';
2
+ import { type ListPage, type StorageAdapter } from './adapter.js';
3
3
  /**
4
4
  * Browser-based Azure Blob Storage adapter.
5
5
  *
@@ -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,5 +1,5 @@
1
1
  import type { FileEntry, WriteResult } from '../types.js';
2
- import type { ListPage, StorageAdapter } from './adapter.js';
2
+ import { type ListPage, type StorageAdapter } from './adapter.js';
3
3
  /**
4
4
  * Browser-based cloud storage adapter (S3-compatible).
5
5
  *
@@ -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
- error = e instanceof Error ? e.message : String(e);
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 fileStore: {
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 };
@@ -40,5 +40,4 @@ function createFilesStore() {
40
40
  }
41
41
  };
42
42
  }
43
- export const fileStore = createFilesStore();
44
- export { fileStore as files };
43
+ export const files = createFilesStore();
@@ -1,5 +1,13 @@
1
1
  import type { Tab } from '../types.js';
2
- export declare const tabStore: {
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 tabStore = createTabsStore();
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;