@blamejs/core 0.7.1 → 0.7.18
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/CHANGELOG.md +423 -389
- package/README.md +150 -149
- package/bin/blamejs.js +0 -0
- package/index.js +308 -282
- package/lib/api-key.js +660 -672
- package/lib/api-snapshot.js +338 -338
- package/lib/app-shutdown.js +385 -385
- package/lib/app.js +365 -365
- package/lib/archive.js +250 -250
- package/lib/atomic-file.js +544 -544
- package/lib/audit-chain.js +177 -177
- package/lib/audit-sign.js +344 -344
- package/lib/audit-tools.js +677 -677
- package/lib/audit.js +766 -766
- package/lib/auth/jwt.js +311 -311
- package/lib/auth/lockout.js +436 -436
- package/lib/auth/oauth.js +721 -721
- package/lib/auth/passkey.js +181 -181
- package/lib/auth/password.js +594 -594
- package/lib/backup/bundle.js +217 -217
- package/lib/backup/crypto.js +176 -176
- package/lib/backup/index.js +515 -515
- package/lib/backup/manifest.js +282 -282
- package/lib/break-glass.js +1338 -1338
- package/lib/bundler.js +441 -441
- package/lib/cache-redis.js +256 -266
- package/lib/cache.js +1206 -1211
- package/lib/canonical-json.js +115 -115
- package/lib/chain-writer.js +234 -234
- package/lib/cli-helpers.js +206 -206
- package/lib/cli.js +2334 -2334
- package/lib/cluster-provider-db.js +317 -317
- package/lib/cluster-storage.js +226 -226
- package/lib/cluster.js +703 -703
- package/lib/codepoint-class.js +196 -0
- package/lib/config-drift.js +301 -301
- package/lib/consent.js +222 -222
- package/lib/constants.js +191 -191
- package/lib/cookies.js +315 -315
- package/lib/credential-hash.js +322 -322
- package/lib/crypto.js +266 -266
- package/lib/csv.js +275 -286
- package/lib/db-declare-row-policy.js +267 -267
- package/lib/db-declare-view.js +420 -421
- package/lib/db-query.js +406 -406
- package/lib/db-schema.js +319 -319
- package/lib/db.js +1288 -1288
- package/lib/deprecate.js +222 -222
- package/lib/dev.js +335 -335
- package/lib/dual-control.js +473 -473
- package/lib/error-page.js +420 -420
- package/lib/external-db-migrate.js +441 -441
- package/lib/external-db.js +1061 -1061
- package/lib/file-type.js +273 -273
- package/lib/file-upload.js +1136 -0
- package/lib/forms.js +422 -422
- package/lib/framework-error.js +293 -202
- package/lib/framework-schema.js +717 -717
- package/lib/gate-contract.js +971 -0
- package/lib/guard-all.js +405 -0
- package/lib/guard-archive.js +739 -0
- package/lib/guard-csv.js +816 -0
- package/lib/guard-email.js +744 -0
- package/lib/guard-filename.js +724 -0
- package/lib/guard-html.js +976 -0
- package/lib/guard-json.js +729 -0
- package/lib/guard-markdown.js +586 -0
- package/lib/guard-svg.js +976 -0
- package/lib/guard-xml.js +405 -0
- package/lib/guard-yaml.js +529 -0
- package/lib/handlers.js +350 -350
- package/lib/http-client-cookie-jar.js +508 -508
- package/lib/http-client.js +1195 -1195
- package/lib/i18n.js +878 -878
- package/lib/jobs.js +185 -185
- package/lib/log-stream-cloudwatch.js +369 -369
- package/lib/log-stream-local.js +146 -146
- package/lib/log-stream-otlp-grpc.js +410 -410
- package/lib/log-stream-otlp.js +286 -286
- package/lib/log-stream-syslog.js +302 -302
- package/lib/log-stream-webhook.js +199 -199
- package/lib/log-stream.js +330 -330
- package/lib/log.js +500 -500
- package/lib/mail-bounce.js +528 -528
- package/lib/mail-dkim.js +369 -362
- package/lib/mail.js +981 -962
- package/lib/metrics.js +683 -683
- package/lib/middleware/api-encrypt.js +936 -573
- package/lib/middleware/attach-user.js +157 -157
- package/lib/middleware/body-parser.js +1170 -1091
- package/lib/middleware/bot-guard.js +178 -178
- package/lib/middleware/compression.js +452 -452
- package/lib/middleware/cors.js +314 -314
- package/lib/middleware/csp-nonce.js +348 -348
- package/lib/middleware/csrf-protect.js +316 -316
- package/lib/middleware/db-role-for.js +264 -269
- package/lib/middleware/health.js +392 -392
- package/lib/middleware/index.js +79 -79
- package/lib/middleware/rate-limit.js +358 -358
- package/lib/middleware/request-id.js +61 -61
- package/lib/middleware/request-log.js +168 -168
- package/lib/middleware/require-auth.js +104 -104
- package/lib/middleware/security-headers.js +116 -116
- package/lib/middleware/sse.js +166 -166
- package/lib/migrations.js +383 -383
- package/lib/mtls-ca.js +518 -518
- package/lib/mtls-engine-default.js +481 -481
- package/lib/network-dns.js +632 -632
- package/lib/network-heartbeat.js +290 -290
- package/lib/network-nts.js +574 -574
- package/lib/network-proxy.js +265 -265
- package/lib/network-tls.js +328 -328
- package/lib/network.js +233 -233
- package/lib/notify.js +612 -614
- package/lib/ntp-check.js +229 -229
- package/lib/numeric-bounds.js +111 -91
- package/lib/object-store/azure-blob-bucket-ops.js +349 -349
- package/lib/object-store/azure-blob.js +488 -451
- package/lib/object-store/gcs-bucket-ops.js +351 -351
- package/lib/object-store/gcs.js +519 -479
- package/lib/object-store/http-put.js +153 -153
- package/lib/object-store/index.js +197 -197
- package/lib/object-store/sigv4-bucket-ops.js +1092 -1092
- package/lib/object-store/sigv4.js +903 -855
- package/lib/observability.js +151 -151
- package/lib/otel-export.js +269 -269
- package/lib/pagination.js +464 -464
- package/lib/parsers/index.js +80 -80
- package/lib/parsers/safe-env.js +642 -642
- package/lib/parsers/safe-ini.js +292 -292
- package/lib/parsers/safe-toml.js +784 -784
- package/lib/parsers/safe-xml.js +390 -390
- package/lib/parsers/safe-yaml.js +1015 -1015
- package/lib/permissions.js +708 -708
- package/lib/pqc-agent.js +87 -87
- package/lib/pqc-gate.js +279 -279
- package/lib/protobuf-encoder.js +190 -190
- package/lib/protocol-dispatcher.js +161 -161
- package/lib/pubsub-redis.js +167 -177
- package/lib/pubsub.js +429 -429
- package/lib/queue-local.js +476 -476
- package/lib/queue-redis.js +745 -752
- package/lib/queue-sqs.js +319 -319
- package/lib/queue.js +695 -695
- package/lib/redis-client.js +519 -489
- package/lib/request-helpers.js +340 -336
- package/lib/restore-bundle.js +237 -237
- package/lib/restore-rollback.js +259 -259
- package/lib/restore.js +409 -409
- package/lib/retry.js +376 -376
- package/lib/router.js +748 -748
- package/lib/safe-async.js +735 -735
- package/lib/safe-buffer.js +237 -237
- package/lib/safe-json.js +541 -541
- package/lib/safe-schema.js +1266 -1266
- package/lib/safe-url.js +159 -159
- package/lib/scheduler.js +706 -706
- package/lib/security-assert.js +373 -373
- package/lib/seeders.js +618 -630
- package/lib/session.js +478 -478
- package/lib/slug.js +269 -269
- package/lib/ssrf-guard.js +401 -401
- package/lib/static.js +879 -114
- package/lib/storage.js +471 -471
- package/lib/subject.js +281 -281
- package/lib/template.js +791 -791
- package/lib/testing.js +798 -798
- package/lib/time.js +310 -310
- package/lib/totp.js +302 -302
- package/lib/tracing.js +494 -494
- package/lib/uuid.js +132 -132
- package/lib/validate-opts.js +340 -270
- package/lib/vault/index.js +308 -308
- package/lib/vault/rotate.js +784 -784
- package/lib/vault/wrap.js +296 -296
- package/lib/vendor/noble-ciphers.cjs +9 -9
- package/lib/webhook.js +595 -598
- package/lib/websocket.js +1048 -1048
- package/package.json +77 -77
- package/sbom.cyclonedx.json +7 -7
|
@@ -1,349 +1,349 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
/**
|
|
3
|
-
* azure-blob-bucket-ops — container-level operations for Azure Blob.
|
|
4
|
-
*
|
|
5
|
-
* Per-blob ops (put / get / list / delete) live in
|
|
6
|
-
* `lib/object-store/azure-blob.js` and are bound to a single container
|
|
7
|
-
* at create() time. Container lifecycle ops are at a different level —
|
|
8
|
-
* a service-scoped client that addresses arbitrary containers — so
|
|
9
|
-
* they get their own factory.
|
|
10
|
-
*
|
|
11
|
-
* create(name, opts?) async; PUT /{container}?restype=container
|
|
12
|
-
* delete(name) async; DELETE /{container}?restype=container
|
|
13
|
-
* list(opts?) async; GET /?comp=list
|
|
14
|
-
* -> [{ name, lastModified, etag,
|
|
15
|
-
* leaseStatus, leaseState, publicAccess }]
|
|
16
|
-
* setCorsRules(rules) async; PUT /?restype=service&comp=properties
|
|
17
|
-
* account-level CORS — Azure has no
|
|
18
|
-
* per-container CORS.
|
|
19
|
-
*
|
|
20
|
-
* Shared Key auth via `lib/object-store/azure-blob.js`'s `signRequest`
|
|
21
|
-
* helper. Not implemented here:
|
|
22
|
-
*
|
|
23
|
-
* setLifecycle(name, rules) — Azure Storage lifecycle management
|
|
24
|
-
* policies live on Azure Resource Manager
|
|
25
|
-
* (`management.azure.com`), not the Blob service endpoint. ARM
|
|
26
|
-
* requires an Azure AD bearer token (Service Principal flow), a
|
|
27
|
-
* different auth scheme entirely. Operators wiring lifecycle do
|
|
28
|
-
* so via Terraform / Bicep / az CLI; the framework documents the
|
|
29
|
-
* gap rather than half-implementing one auth path.
|
|
30
|
-
*/
|
|
31
|
-
var azureBlob = require("./azure-blob");
|
|
32
|
-
var C = require("../constants");
|
|
33
|
-
var httpClient = require("../http-client");
|
|
34
|
-
var requestHelpers = require("../request-helpers");
|
|
35
|
-
var safeUrl = require("../safe-url");
|
|
36
|
-
var { ObjectStoreError } = require("../framework-error");
|
|
37
|
-
|
|
38
|
-
var _err = ObjectStoreError.factory;
|
|
39
|
-
|
|
40
|
-
// HTTP status constants used in the bucket-ops expectStatus arrays.
|
|
41
|
-
// OK / NOT_FOUND / CONFLICT come from the framework's HTTP_STATUS table;
|
|
42
|
-
// CREATED (201) and ACCEPTED (202) are not in the table, so they're
|
|
43
|
-
// named locally with their RFC 7231 values.
|
|
44
|
-
var HTTP_OK = requestHelpers.HTTP_STATUS.OK;
|
|
45
|
-
var HTTP_NOT_FOUND = requestHelpers.HTTP_STATUS.NOT_FOUND;
|
|
46
|
-
var HTTP_CONFLICT = requestHelpers.HTTP_STATUS.CONFLICT;
|
|
47
|
-
var HTTP_CREATED = 201; // RFC 7231 §6.3.2
|
|
48
|
-
var HTTP_ACCEPTED = 202; // RFC 7231 §6.3.3
|
|
49
|
-
|
|
50
|
-
// Internal URL builder — endpoint + path string from validated config.
|
|
51
|
-
// Routes through safeUrl.parse so the protocol allowlist + length cap
|
|
52
|
-
// apply uniformly.
|
|
53
|
-
function _internalUrl(input, allowedProtocols) {
|
|
54
|
-
return safeUrl.parse(input, {
|
|
55
|
-
allowedProtocols: allowedProtocols || safeUrl.ALLOW_HTTP_TLS,
|
|
56
|
-
errorClass: ObjectStoreError,
|
|
57
|
-
maxUrlLength: C.BYTES.kib(32),
|
|
58
|
-
});
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
// Maximum length for a tag passed into the framework-internal XML
|
|
62
|
-
// extractor. The XML payloads come back from Azure (server-controlled)
|
|
63
|
-
// but the tag name is framework-supplied; cap it explicitly so the
|
|
64
|
-
// open/close substrings used for indexOf walks can't be driven against
|
|
65
|
-
// an oversize pattern.
|
|
66
|
-
var MAX_XML_TAG_LENGTH = 128;
|
|
67
|
-
|
|
68
|
-
// Azure container names: 3-63 chars, lowercase alphanumeric + hyphens,
|
|
69
|
-
// no consecutive hyphens, must start and end with letter or digit.
|
|
70
|
-
var CONTAINER_NAME_RE = /^[a-z0-9](?:[a-z0-9]|-(?!-))*[a-z0-9]$/;
|
|
71
|
-
|
|
72
|
-
function _validateContainerName(name) {
|
|
73
|
-
if (typeof name !== "string" || name.length === 0) {
|
|
74
|
-
throw _err("BUCKET_INVALID_NAME",
|
|
75
|
-
"azure-blob bucketOps: container name must be a non-empty string", true);
|
|
76
|
-
}
|
|
77
|
-
if (name.length < 3 || name.length > 63) {
|
|
78
|
-
throw _err("BUCKET_INVALID_NAME",
|
|
79
|
-
"azure-blob bucketOps: container name must be 3-63 chars (got " +
|
|
80
|
-
name.length + ")", true);
|
|
81
|
-
}
|
|
82
|
-
// Length is bounded above (3..63) before the regex runs so the
|
|
83
|
-
// pattern can't be driven against an unbounded input.
|
|
84
|
-
if (name.length > 63 || !CONTAINER_NAME_RE.test(name)) {
|
|
85
|
-
throw _err("BUCKET_INVALID_NAME",
|
|
86
|
-
"azure-blob bucketOps: container name '" + name + "' is invalid; " +
|
|
87
|
-
"lowercase letters / digits / hyphens only, no consecutive hyphens, " +
|
|
88
|
-
"must start and end with letter or digit", true);
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
function _validateCorsRule(rule, idx) {
|
|
93
|
-
function bad(msg) {
|
|
94
|
-
throw _err("INVALID_CORS_RULE",
|
|
95
|
-
"azure-blob bucketOps: setCorsRules: rule[" + idx + "]: " + msg, true);
|
|
96
|
-
}
|
|
97
|
-
if (!rule || typeof rule !== "object") bad("must be an object");
|
|
98
|
-
if (!Array.isArray(rule.allowedOrigins) || rule.allowedOrigins.length === 0) {
|
|
99
|
-
bad("allowedOrigins must be a non-empty array");
|
|
100
|
-
}
|
|
101
|
-
if (!Array.isArray(rule.allowedMethods) || rule.allowedMethods.length === 0) {
|
|
102
|
-
bad("allowedMethods must be a non-empty array");
|
|
103
|
-
}
|
|
104
|
-
for (var i = 0; i < rule.allowedMethods.length; i++) {
|
|
105
|
-
var m = rule.allowedMethods[i];
|
|
106
|
-
if (["GET", "PUT", "POST", "DELETE", "HEAD", "MERGE", "OPTIONS"].indexOf(m) === -1) {
|
|
107
|
-
bad("allowedMethods[" + i + "] = " + JSON.stringify(m) +
|
|
108
|
-
" (must be one of GET/PUT/POST/DELETE/HEAD/MERGE/OPTIONS)");
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
if (rule.allowedHeaders !== undefined && !Array.isArray(rule.allowedHeaders)) {
|
|
112
|
-
bad("allowedHeaders, if present, must be an array");
|
|
113
|
-
}
|
|
114
|
-
if (rule.exposedHeaders !== undefined && !Array.isArray(rule.exposedHeaders)) {
|
|
115
|
-
bad("exposedHeaders, if present, must be an array");
|
|
116
|
-
}
|
|
117
|
-
if (rule.maxAgeInSeconds !== undefined &&
|
|
118
|
-
(typeof rule.maxAgeInSeconds !== "number" || rule.maxAgeInSeconds < 0 ||
|
|
119
|
-
!Number.isFinite(rule.maxAgeInSeconds))) {
|
|
120
|
-
bad("maxAgeInSeconds, if present, must be a non-negative finite number");
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
function _xmlEscape(s) {
|
|
125
|
-
return String(s)
|
|
126
|
-
.replace(/&/g, "&")
|
|
127
|
-
.replace(/</g, "<")
|
|
128
|
-
.replace(/>/g, ">")
|
|
129
|
-
.replace(/"/g, """)
|
|
130
|
-
.replace(/'/g, "'");
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
function _buildCorsXml(rules) {
|
|
134
|
-
var inner = rules.map(function (rule) {
|
|
135
|
-
var parts = [
|
|
136
|
-
"<CorsRule>",
|
|
137
|
-
"<AllowedOrigins>" + _xmlEscape(rule.allowedOrigins.join(",")) + "</AllowedOrigins>",
|
|
138
|
-
"<AllowedMethods>" + rule.allowedMethods.join(",") + "</AllowedMethods>",
|
|
139
|
-
"<AllowedHeaders>" + _xmlEscape((rule.allowedHeaders || []).join(",")) + "</AllowedHeaders>",
|
|
140
|
-
"<ExposedHeaders>" + _xmlEscape((rule.exposedHeaders || []).join(",")) + "</ExposedHeaders>",
|
|
141
|
-
"<MaxAgeInSeconds>" +
|
|
142
|
-
(rule.maxAgeInSeconds == null ? 0 : Math.floor(rule.maxAgeInSeconds)) +
|
|
143
|
-
"</MaxAgeInSeconds>",
|
|
144
|
-
"</CorsRule>",
|
|
145
|
-
];
|
|
146
|
-
return parts.join("");
|
|
147
|
-
}).join("");
|
|
148
|
-
return '<?xml version="1.0" encoding="utf-8"?>' +
|
|
149
|
-
"<StorageServiceProperties><Cors>" + inner + "</Cors></StorageServiceProperties>";
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
// Tiny XML extractor — pulls every occurrence of <Tag>value</Tag>
|
|
153
|
-
// and returns an array of value strings. Sufficient for the limited
|
|
154
|
-
// shapes we read (Containers list, container metadata). The tag is
|
|
155
|
-
// framework-supplied (call sites pass hardcoded tag names such as
|
|
156
|
-
// "Container", "Name", "Last-Modified") but cap the length explicitly
|
|
157
|
-
// so the open/close substrings used for indexOf walks can't be driven
|
|
158
|
-
// against an oversize pattern under any future call shape.
|
|
159
|
-
function _validateTag(tag) {
|
|
160
|
-
if (typeof tag !== "string" || tag.length === 0 || tag.length > MAX_XML_TAG_LENGTH) {
|
|
161
|
-
throw _err("BUCKET_INVALID_NAME",
|
|
162
|
-
"azure-blob bucketOps: XML tag must be a non-empty string of length " +
|
|
163
|
-
"<= " + MAX_XML_TAG_LENGTH, true);
|
|
164
|
-
}
|
|
165
|
-
if (!/^[A-Za-z][A-Za-z0-9_.-]*$/.test(tag)) {
|
|
166
|
-
throw _err("BUCKET_INVALID_NAME",
|
|
167
|
-
"azure-blob bucketOps: XML tag '" + tag + "' has invalid characters", true);
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
function _extractAll(xml, tag) {
|
|
172
|
-
_validateTag(tag);
|
|
173
|
-
var open = "<" + tag + ">";
|
|
174
|
-
var close = "</" + tag + ">";
|
|
175
|
-
var out = [];
|
|
176
|
-
var i = 0;
|
|
177
|
-
while (true) {
|
|
178
|
-
var s = xml.indexOf(open, i);
|
|
179
|
-
if (s === -1) break;
|
|
180
|
-
var e = xml.indexOf(close, s + open.length);
|
|
181
|
-
if (e === -1) break;
|
|
182
|
-
out.push(xml.slice(s + open.length, e));
|
|
183
|
-
i = e + close.length;
|
|
184
|
-
}
|
|
185
|
-
return out;
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
function _extractBlocks(xml, tag) {
|
|
189
|
-
_validateTag(tag);
|
|
190
|
-
var open = "<" + tag + ">";
|
|
191
|
-
var close = "</" + tag + ">";
|
|
192
|
-
var blocks = [];
|
|
193
|
-
var i = 0;
|
|
194
|
-
while (true) {
|
|
195
|
-
var s = xml.indexOf(open, i);
|
|
196
|
-
if (s === -1) break;
|
|
197
|
-
var e = xml.indexOf(close, s + open.length);
|
|
198
|
-
if (e === -1) break;
|
|
199
|
-
blocks.push(xml.slice(s + open.length, e));
|
|
200
|
-
i = e + close.length;
|
|
201
|
-
}
|
|
202
|
-
return blocks;
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
function create(config) {
|
|
206
|
-
if (!config) throw _err("BAD_OPT", "azure-blob bucketOps: config required", true);
|
|
207
|
-
if (!config.accountName) throw _err("BAD_OPT", "azure-blob bucketOps: accountName required", true);
|
|
208
|
-
if (!config.accountKey) throw _err("BAD_OPT", "azure-blob bucketOps: accountKey required", true);
|
|
209
|
-
|
|
210
|
-
var endpoint = config.endpoint ||
|
|
211
|
-
("https://" + config.accountName + ".blob.core.windows.net");
|
|
212
|
-
if (endpoint.endsWith("/")) endpoint = endpoint.slice(0, -1);
|
|
213
|
-
var apiVersion = config.apiVersion || azureBlob.DEFAULT_API_VERSION;
|
|
214
|
-
var timeoutMs = config.timeoutMs;
|
|
215
|
-
var allowedProtocols = config.allowedProtocols || safeUrl.ALLOW_HTTP_TLS;
|
|
216
|
-
var allowInternal = config.allowInternal != null ? config.allowInternal : null;
|
|
217
|
-
|
|
218
|
-
function _sign(method, url, headers) {
|
|
219
|
-
return azureBlob.signRequest({
|
|
220
|
-
method: method,
|
|
221
|
-
url: url,
|
|
222
|
-
headers: headers || {},
|
|
223
|
-
accountName: config.accountName,
|
|
224
|
-
accountKey: config.accountKey,
|
|
225
|
-
apiVersion: apiVersion,
|
|
226
|
-
}).headers;
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
function _request(method, url, headers, body, expectStatus) {
|
|
230
|
-
var reqOpts = {
|
|
231
|
-
method: method,
|
|
232
|
-
url: url,
|
|
233
|
-
headers: headers,
|
|
234
|
-
body: body,
|
|
235
|
-
idleTimeoutMs: timeoutMs,
|
|
236
|
-
allowedProtocols: allowedProtocols,
|
|
237
|
-
errorClass: ObjectStoreError,
|
|
238
|
-
};
|
|
239
|
-
if (allowInternal !== null) reqOpts.allowInternal = allowInternal;
|
|
240
|
-
// http-client rejects on any 4xx/5xx by default. The bucket-ops API
|
|
241
|
-
// semantically accepts certain non-2xx codes (404 for "missing on
|
|
242
|
-
// delete", 409 for "container already exists"), so we catch those
|
|
243
|
-
// and surface them as the wrapped response object instead.
|
|
244
|
-
return httpClient.request(reqOpts).then(function (res) {
|
|
245
|
-
if (expectStatus && expectStatus.indexOf(res.statusCode) === -1) {
|
|
246
|
-
throw _err("UNEXPECTED_STATUS",
|
|
247
|
-
"azure-blob bucketOps: " + method + " " + url +
|
|
248
|
-
" returned HTTP " + res.statusCode, true);
|
|
249
|
-
}
|
|
250
|
-
return res;
|
|
251
|
-
}, function (e) {
|
|
252
|
-
var sc = e && e.statusCode;
|
|
253
|
-
if (sc && expectStatus && expectStatus.indexOf(sc) !== -1) {
|
|
254
|
-
return { statusCode: sc, headers: {}, body: Buffer.alloc(0) };
|
|
255
|
-
}
|
|
256
|
-
throw e;
|
|
257
|
-
});
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
async function createContainer(name, opts) {
|
|
261
|
-
_validateContainerName(name);
|
|
262
|
-
opts = opts || {};
|
|
263
|
-
var url = _internalUrl(endpoint + "/" + name + "?restype=container", allowedProtocols);
|
|
264
|
-
var headers = { "Content-Length": "0" };
|
|
265
|
-
if (opts.publicAccess) {
|
|
266
|
-
if (opts.publicAccess !== "blob" && opts.publicAccess !== "container") {
|
|
267
|
-
throw _err("BAD_OPT",
|
|
268
|
-
"azure-blob bucketOps: createContainer: publicAccess must be " +
|
|
269
|
-
"'blob' or 'container' (got " + JSON.stringify(opts.publicAccess) + ")", true);
|
|
270
|
-
}
|
|
271
|
-
headers["x-ms-blob-public-access"] = opts.publicAccess;
|
|
272
|
-
}
|
|
273
|
-
var signed = _sign("PUT", url, headers);
|
|
274
|
-
var res = await _request("PUT", url, signed, null, [HTTP_CREATED, HTTP_CONFLICT]);
|
|
275
|
-
if (res.statusCode === HTTP_CONFLICT) {
|
|
276
|
-
throw _err("BUCKET_ALREADY_OWNED",
|
|
277
|
-
"azure-blob bucketOps: container '" + name +
|
|
278
|
-
"' already exists or was recently deleted", true);
|
|
279
|
-
}
|
|
280
|
-
return { name: name };
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
async function deleteContainer(name) {
|
|
284
|
-
_validateContainerName(name);
|
|
285
|
-
var url = _internalUrl(endpoint + "/" + name + "?restype=container", allowedProtocols);
|
|
286
|
-
var signed = _sign("DELETE", url, {});
|
|
287
|
-
var res = await _request("DELETE", url, signed, null, [HTTP_ACCEPTED, HTTP_NOT_FOUND]);
|
|
288
|
-
return res.statusCode === HTTP_ACCEPTED;
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
async function listContainers(opts) {
|
|
292
|
-
opts = opts || {};
|
|
293
|
-
var url = _internalUrl(endpoint + "/?comp=list", allowedProtocols);
|
|
294
|
-
if (opts.prefix) url.searchParams.set("prefix", opts.prefix);
|
|
295
|
-
if (opts.maxResults != null) url.searchParams.set("maxresults", String(opts.maxResults));
|
|
296
|
-
var signed = _sign("GET", url, {});
|
|
297
|
-
var res = await _request("GET", url, signed, null, [HTTP_OK]);
|
|
298
|
-
var xml = Buffer.isBuffer(res.body) ? res.body.toString("utf8") :
|
|
299
|
-
typeof res.body === "string" ? res.body :
|
|
300
|
-
"";
|
|
301
|
-
var blocks = _extractBlocks(xml, "Container");
|
|
302
|
-
return blocks.map(function (block) {
|
|
303
|
-
return {
|
|
304
|
-
name: (_extractAll(block, "Name")[0] || "").trim(),
|
|
305
|
-
lastModified: (_extractAll(block, "Last-Modified")[0] || null),
|
|
306
|
-
etag: (_extractAll(block, "Etag")[0] || null),
|
|
307
|
-
leaseStatus: (_extractAll(block, "LeaseStatus")[0] || null),
|
|
308
|
-
leaseState: (_extractAll(block, "LeaseState")[0] || null),
|
|
309
|
-
publicAccess: (_extractAll(block, "PublicAccess")[0] || null),
|
|
310
|
-
};
|
|
311
|
-
});
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
async function setCorsRules(rules) {
|
|
315
|
-
if (!Array.isArray(rules)) {
|
|
316
|
-
throw _err("INVALID_CORS_RULE",
|
|
317
|
-
"azure-blob bucketOps: setCorsRules: rules must be an array", true);
|
|
318
|
-
}
|
|
319
|
-
rules.forEach(_validateCorsRule);
|
|
320
|
-
var xml = _buildCorsXml(rules);
|
|
321
|
-
var bodyBuf = Buffer.from(xml, "utf8");
|
|
322
|
-
var url = _internalUrl(endpoint + "/?restype=service&comp=properties", allowedProtocols);
|
|
323
|
-
var headers = {
|
|
324
|
-
"Content-Type": "application/xml",
|
|
325
|
-
"Content-Length": String(bodyBuf.length),
|
|
326
|
-
};
|
|
327
|
-
var signed = _sign("PUT", url, headers);
|
|
328
|
-
await _request("PUT", url, signed, bodyBuf, [HTTP_ACCEPTED]);
|
|
329
|
-
return { rulesApplied: rules.length };
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
return {
|
|
333
|
-
protocol: "azure-blob",
|
|
334
|
-
create: createContainer,
|
|
335
|
-
delete: deleteContainer,
|
|
336
|
-
list: listContainers,
|
|
337
|
-
setCorsRules: setCorsRules,
|
|
338
|
-
setLifecycle: function () {
|
|
339
|
-
throw _err("NOT_SUPPORTED",
|
|
340
|
-
"azure-blob bucketOps: setLifecycle is not implemented because " +
|
|
341
|
-
"Azure Storage lifecycle management policies live on Azure Resource " +
|
|
342
|
-
"Manager (management.azure.com) and require Azure AD bearer token " +
|
|
343
|
-
"auth, not Shared Key. Configure lifecycle via Terraform / Bicep / " +
|
|
344
|
-
"az CLI.", true);
|
|
345
|
-
},
|
|
346
|
-
};
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
module.exports = { create: create };
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* azure-blob-bucket-ops — container-level operations for Azure Blob.
|
|
4
|
+
*
|
|
5
|
+
* Per-blob ops (put / get / list / delete) live in
|
|
6
|
+
* `lib/object-store/azure-blob.js` and are bound to a single container
|
|
7
|
+
* at create() time. Container lifecycle ops are at a different level —
|
|
8
|
+
* a service-scoped client that addresses arbitrary containers — so
|
|
9
|
+
* they get their own factory.
|
|
10
|
+
*
|
|
11
|
+
* create(name, opts?) async; PUT /{container}?restype=container
|
|
12
|
+
* delete(name) async; DELETE /{container}?restype=container
|
|
13
|
+
* list(opts?) async; GET /?comp=list
|
|
14
|
+
* -> [{ name, lastModified, etag,
|
|
15
|
+
* leaseStatus, leaseState, publicAccess }]
|
|
16
|
+
* setCorsRules(rules) async; PUT /?restype=service&comp=properties
|
|
17
|
+
* account-level CORS — Azure has no
|
|
18
|
+
* per-container CORS.
|
|
19
|
+
*
|
|
20
|
+
* Shared Key auth via `lib/object-store/azure-blob.js`'s `signRequest`
|
|
21
|
+
* helper. Not implemented here:
|
|
22
|
+
*
|
|
23
|
+
* setLifecycle(name, rules) — Azure Storage lifecycle management
|
|
24
|
+
* policies live on Azure Resource Manager
|
|
25
|
+
* (`management.azure.com`), not the Blob service endpoint. ARM
|
|
26
|
+
* requires an Azure AD bearer token (Service Principal flow), a
|
|
27
|
+
* different auth scheme entirely. Operators wiring lifecycle do
|
|
28
|
+
* so via Terraform / Bicep / az CLI; the framework documents the
|
|
29
|
+
* gap rather than half-implementing one auth path.
|
|
30
|
+
*/
|
|
31
|
+
var azureBlob = require("./azure-blob");
|
|
32
|
+
var C = require("../constants");
|
|
33
|
+
var httpClient = require("../http-client");
|
|
34
|
+
var requestHelpers = require("../request-helpers");
|
|
35
|
+
var safeUrl = require("../safe-url");
|
|
36
|
+
var { ObjectStoreError } = require("../framework-error");
|
|
37
|
+
|
|
38
|
+
var _err = ObjectStoreError.factory;
|
|
39
|
+
|
|
40
|
+
// HTTP status constants used in the bucket-ops expectStatus arrays.
|
|
41
|
+
// OK / NOT_FOUND / CONFLICT come from the framework's HTTP_STATUS table;
|
|
42
|
+
// CREATED (201) and ACCEPTED (202) are not in the table, so they're
|
|
43
|
+
// named locally with their RFC 7231 values.
|
|
44
|
+
var HTTP_OK = requestHelpers.HTTP_STATUS.OK;
|
|
45
|
+
var HTTP_NOT_FOUND = requestHelpers.HTTP_STATUS.NOT_FOUND;
|
|
46
|
+
var HTTP_CONFLICT = requestHelpers.HTTP_STATUS.CONFLICT;
|
|
47
|
+
var HTTP_CREATED = 201; // RFC 7231 §6.3.2
|
|
48
|
+
var HTTP_ACCEPTED = 202; // RFC 7231 §6.3.3
|
|
49
|
+
|
|
50
|
+
// Internal URL builder — endpoint + path string from validated config.
|
|
51
|
+
// Routes through safeUrl.parse so the protocol allowlist + length cap
|
|
52
|
+
// apply uniformly.
|
|
53
|
+
function _internalUrl(input, allowedProtocols) {
|
|
54
|
+
return safeUrl.parse(input, {
|
|
55
|
+
allowedProtocols: allowedProtocols || safeUrl.ALLOW_HTTP_TLS,
|
|
56
|
+
errorClass: ObjectStoreError,
|
|
57
|
+
maxUrlLength: C.BYTES.kib(32),
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Maximum length for a tag passed into the framework-internal XML
|
|
62
|
+
// extractor. The XML payloads come back from Azure (server-controlled)
|
|
63
|
+
// but the tag name is framework-supplied; cap it explicitly so the
|
|
64
|
+
// open/close substrings used for indexOf walks can't be driven against
|
|
65
|
+
// an oversize pattern.
|
|
66
|
+
var MAX_XML_TAG_LENGTH = 128;
|
|
67
|
+
|
|
68
|
+
// Azure container names: 3-63 chars, lowercase alphanumeric + hyphens,
|
|
69
|
+
// no consecutive hyphens, must start and end with letter or digit.
|
|
70
|
+
var CONTAINER_NAME_RE = /^[a-z0-9](?:[a-z0-9]|-(?!-))*[a-z0-9]$/;
|
|
71
|
+
|
|
72
|
+
function _validateContainerName(name) {
|
|
73
|
+
if (typeof name !== "string" || name.length === 0) {
|
|
74
|
+
throw _err("BUCKET_INVALID_NAME",
|
|
75
|
+
"azure-blob bucketOps: container name must be a non-empty string", true);
|
|
76
|
+
}
|
|
77
|
+
if (name.length < 3 || name.length > 63) {
|
|
78
|
+
throw _err("BUCKET_INVALID_NAME",
|
|
79
|
+
"azure-blob bucketOps: container name must be 3-63 chars (got " +
|
|
80
|
+
name.length + ")", true);
|
|
81
|
+
}
|
|
82
|
+
// Length is bounded above (3..63) before the regex runs so the
|
|
83
|
+
// pattern can't be driven against an unbounded input.
|
|
84
|
+
if (name.length > 63 || !CONTAINER_NAME_RE.test(name)) {
|
|
85
|
+
throw _err("BUCKET_INVALID_NAME",
|
|
86
|
+
"azure-blob bucketOps: container name '" + name + "' is invalid; " +
|
|
87
|
+
"lowercase letters / digits / hyphens only, no consecutive hyphens, " +
|
|
88
|
+
"must start and end with letter or digit", true);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function _validateCorsRule(rule, idx) {
|
|
93
|
+
function bad(msg) {
|
|
94
|
+
throw _err("INVALID_CORS_RULE",
|
|
95
|
+
"azure-blob bucketOps: setCorsRules: rule[" + idx + "]: " + msg, true);
|
|
96
|
+
}
|
|
97
|
+
if (!rule || typeof rule !== "object") bad("must be an object");
|
|
98
|
+
if (!Array.isArray(rule.allowedOrigins) || rule.allowedOrigins.length === 0) {
|
|
99
|
+
bad("allowedOrigins must be a non-empty array");
|
|
100
|
+
}
|
|
101
|
+
if (!Array.isArray(rule.allowedMethods) || rule.allowedMethods.length === 0) {
|
|
102
|
+
bad("allowedMethods must be a non-empty array");
|
|
103
|
+
}
|
|
104
|
+
for (var i = 0; i < rule.allowedMethods.length; i++) {
|
|
105
|
+
var m = rule.allowedMethods[i];
|
|
106
|
+
if (["GET", "PUT", "POST", "DELETE", "HEAD", "MERGE", "OPTIONS"].indexOf(m) === -1) {
|
|
107
|
+
bad("allowedMethods[" + i + "] = " + JSON.stringify(m) +
|
|
108
|
+
" (must be one of GET/PUT/POST/DELETE/HEAD/MERGE/OPTIONS)");
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
if (rule.allowedHeaders !== undefined && !Array.isArray(rule.allowedHeaders)) {
|
|
112
|
+
bad("allowedHeaders, if present, must be an array");
|
|
113
|
+
}
|
|
114
|
+
if (rule.exposedHeaders !== undefined && !Array.isArray(rule.exposedHeaders)) {
|
|
115
|
+
bad("exposedHeaders, if present, must be an array");
|
|
116
|
+
}
|
|
117
|
+
if (rule.maxAgeInSeconds !== undefined &&
|
|
118
|
+
(typeof rule.maxAgeInSeconds !== "number" || rule.maxAgeInSeconds < 0 ||
|
|
119
|
+
!Number.isFinite(rule.maxAgeInSeconds))) {
|
|
120
|
+
bad("maxAgeInSeconds, if present, must be a non-negative finite number");
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function _xmlEscape(s) {
|
|
125
|
+
return String(s)
|
|
126
|
+
.replace(/&/g, "&")
|
|
127
|
+
.replace(/</g, "<")
|
|
128
|
+
.replace(/>/g, ">")
|
|
129
|
+
.replace(/"/g, """)
|
|
130
|
+
.replace(/'/g, "'");
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function _buildCorsXml(rules) {
|
|
134
|
+
var inner = rules.map(function (rule) {
|
|
135
|
+
var parts = [
|
|
136
|
+
"<CorsRule>",
|
|
137
|
+
"<AllowedOrigins>" + _xmlEscape(rule.allowedOrigins.join(",")) + "</AllowedOrigins>",
|
|
138
|
+
"<AllowedMethods>" + rule.allowedMethods.join(",") + "</AllowedMethods>",
|
|
139
|
+
"<AllowedHeaders>" + _xmlEscape((rule.allowedHeaders || []).join(",")) + "</AllowedHeaders>",
|
|
140
|
+
"<ExposedHeaders>" + _xmlEscape((rule.exposedHeaders || []).join(",")) + "</ExposedHeaders>",
|
|
141
|
+
"<MaxAgeInSeconds>" +
|
|
142
|
+
(rule.maxAgeInSeconds == null ? 0 : Math.floor(rule.maxAgeInSeconds)) +
|
|
143
|
+
"</MaxAgeInSeconds>",
|
|
144
|
+
"</CorsRule>",
|
|
145
|
+
];
|
|
146
|
+
return parts.join("");
|
|
147
|
+
}).join("");
|
|
148
|
+
return '<?xml version="1.0" encoding="utf-8"?>' +
|
|
149
|
+
"<StorageServiceProperties><Cors>" + inner + "</Cors></StorageServiceProperties>";
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Tiny XML extractor — pulls every occurrence of <Tag>value</Tag>
|
|
153
|
+
// and returns an array of value strings. Sufficient for the limited
|
|
154
|
+
// shapes we read (Containers list, container metadata). The tag is
|
|
155
|
+
// framework-supplied (call sites pass hardcoded tag names such as
|
|
156
|
+
// "Container", "Name", "Last-Modified") but cap the length explicitly
|
|
157
|
+
// so the open/close substrings used for indexOf walks can't be driven
|
|
158
|
+
// against an oversize pattern under any future call shape.
|
|
159
|
+
function _validateTag(tag) {
|
|
160
|
+
if (typeof tag !== "string" || tag.length === 0 || tag.length > MAX_XML_TAG_LENGTH) {
|
|
161
|
+
throw _err("BUCKET_INVALID_NAME",
|
|
162
|
+
"azure-blob bucketOps: XML tag must be a non-empty string of length " +
|
|
163
|
+
"<= " + MAX_XML_TAG_LENGTH, true);
|
|
164
|
+
}
|
|
165
|
+
if (!/^[A-Za-z][A-Za-z0-9_.-]*$/.test(tag)) {
|
|
166
|
+
throw _err("BUCKET_INVALID_NAME",
|
|
167
|
+
"azure-blob bucketOps: XML tag '" + tag + "' has invalid characters", true);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function _extractAll(xml, tag) {
|
|
172
|
+
_validateTag(tag);
|
|
173
|
+
var open = "<" + tag + ">";
|
|
174
|
+
var close = "</" + tag + ">";
|
|
175
|
+
var out = [];
|
|
176
|
+
var i = 0;
|
|
177
|
+
while (true) {
|
|
178
|
+
var s = xml.indexOf(open, i);
|
|
179
|
+
if (s === -1) break;
|
|
180
|
+
var e = xml.indexOf(close, s + open.length);
|
|
181
|
+
if (e === -1) break;
|
|
182
|
+
out.push(xml.slice(s + open.length, e));
|
|
183
|
+
i = e + close.length;
|
|
184
|
+
}
|
|
185
|
+
return out;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function _extractBlocks(xml, tag) {
|
|
189
|
+
_validateTag(tag);
|
|
190
|
+
var open = "<" + tag + ">";
|
|
191
|
+
var close = "</" + tag + ">";
|
|
192
|
+
var blocks = [];
|
|
193
|
+
var i = 0;
|
|
194
|
+
while (true) {
|
|
195
|
+
var s = xml.indexOf(open, i);
|
|
196
|
+
if (s === -1) break;
|
|
197
|
+
var e = xml.indexOf(close, s + open.length);
|
|
198
|
+
if (e === -1) break;
|
|
199
|
+
blocks.push(xml.slice(s + open.length, e));
|
|
200
|
+
i = e + close.length;
|
|
201
|
+
}
|
|
202
|
+
return blocks;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function create(config) {
|
|
206
|
+
if (!config) throw _err("BAD_OPT", "azure-blob bucketOps: config required", true);
|
|
207
|
+
if (!config.accountName) throw _err("BAD_OPT", "azure-blob bucketOps: accountName required", true);
|
|
208
|
+
if (!config.accountKey) throw _err("BAD_OPT", "azure-blob bucketOps: accountKey required", true);
|
|
209
|
+
|
|
210
|
+
var endpoint = config.endpoint ||
|
|
211
|
+
("https://" + config.accountName + ".blob.core.windows.net");
|
|
212
|
+
if (endpoint.endsWith("/")) endpoint = endpoint.slice(0, -1);
|
|
213
|
+
var apiVersion = config.apiVersion || azureBlob.DEFAULT_API_VERSION;
|
|
214
|
+
var timeoutMs = config.timeoutMs;
|
|
215
|
+
var allowedProtocols = config.allowedProtocols || safeUrl.ALLOW_HTTP_TLS;
|
|
216
|
+
var allowInternal = config.allowInternal != null ? config.allowInternal : null;
|
|
217
|
+
|
|
218
|
+
function _sign(method, url, headers) {
|
|
219
|
+
return azureBlob.signRequest({
|
|
220
|
+
method: method,
|
|
221
|
+
url: url,
|
|
222
|
+
headers: headers || {},
|
|
223
|
+
accountName: config.accountName,
|
|
224
|
+
accountKey: config.accountKey,
|
|
225
|
+
apiVersion: apiVersion,
|
|
226
|
+
}).headers;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function _request(method, url, headers, body, expectStatus) {
|
|
230
|
+
var reqOpts = {
|
|
231
|
+
method: method,
|
|
232
|
+
url: url,
|
|
233
|
+
headers: headers,
|
|
234
|
+
body: body,
|
|
235
|
+
idleTimeoutMs: timeoutMs,
|
|
236
|
+
allowedProtocols: allowedProtocols,
|
|
237
|
+
errorClass: ObjectStoreError,
|
|
238
|
+
};
|
|
239
|
+
if (allowInternal !== null) reqOpts.allowInternal = allowInternal;
|
|
240
|
+
// http-client rejects on any 4xx/5xx by default. The bucket-ops API
|
|
241
|
+
// semantically accepts certain non-2xx codes (404 for "missing on
|
|
242
|
+
// delete", 409 for "container already exists"), so we catch those
|
|
243
|
+
// and surface them as the wrapped response object instead.
|
|
244
|
+
return httpClient.request(reqOpts).then(function (res) {
|
|
245
|
+
if (expectStatus && expectStatus.indexOf(res.statusCode) === -1) {
|
|
246
|
+
throw _err("UNEXPECTED_STATUS",
|
|
247
|
+
"azure-blob bucketOps: " + method + " " + url +
|
|
248
|
+
" returned HTTP " + res.statusCode, true);
|
|
249
|
+
}
|
|
250
|
+
return res;
|
|
251
|
+
}, function (e) {
|
|
252
|
+
var sc = e && e.statusCode;
|
|
253
|
+
if (sc && expectStatus && expectStatus.indexOf(sc) !== -1) {
|
|
254
|
+
return { statusCode: sc, headers: {}, body: Buffer.alloc(0) };
|
|
255
|
+
}
|
|
256
|
+
throw e;
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
async function createContainer(name, opts) {
|
|
261
|
+
_validateContainerName(name);
|
|
262
|
+
opts = opts || {};
|
|
263
|
+
var url = _internalUrl(endpoint + "/" + name + "?restype=container", allowedProtocols);
|
|
264
|
+
var headers = { "Content-Length": "0" };
|
|
265
|
+
if (opts.publicAccess) {
|
|
266
|
+
if (opts.publicAccess !== "blob" && opts.publicAccess !== "container") {
|
|
267
|
+
throw _err("BAD_OPT",
|
|
268
|
+
"azure-blob bucketOps: createContainer: publicAccess must be " +
|
|
269
|
+
"'blob' or 'container' (got " + JSON.stringify(opts.publicAccess) + ")", true);
|
|
270
|
+
}
|
|
271
|
+
headers["x-ms-blob-public-access"] = opts.publicAccess;
|
|
272
|
+
}
|
|
273
|
+
var signed = _sign("PUT", url, headers);
|
|
274
|
+
var res = await _request("PUT", url, signed, null, [HTTP_CREATED, HTTP_CONFLICT]);
|
|
275
|
+
if (res.statusCode === HTTP_CONFLICT) {
|
|
276
|
+
throw _err("BUCKET_ALREADY_OWNED",
|
|
277
|
+
"azure-blob bucketOps: container '" + name +
|
|
278
|
+
"' already exists or was recently deleted", true);
|
|
279
|
+
}
|
|
280
|
+
return { name: name };
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
async function deleteContainer(name) {
|
|
284
|
+
_validateContainerName(name);
|
|
285
|
+
var url = _internalUrl(endpoint + "/" + name + "?restype=container", allowedProtocols);
|
|
286
|
+
var signed = _sign("DELETE", url, {});
|
|
287
|
+
var res = await _request("DELETE", url, signed, null, [HTTP_ACCEPTED, HTTP_NOT_FOUND]);
|
|
288
|
+
return res.statusCode === HTTP_ACCEPTED;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
async function listContainers(opts) {
|
|
292
|
+
opts = opts || {};
|
|
293
|
+
var url = _internalUrl(endpoint + "/?comp=list", allowedProtocols);
|
|
294
|
+
if (opts.prefix) url.searchParams.set("prefix", opts.prefix);
|
|
295
|
+
if (opts.maxResults != null) url.searchParams.set("maxresults", String(opts.maxResults));
|
|
296
|
+
var signed = _sign("GET", url, {});
|
|
297
|
+
var res = await _request("GET", url, signed, null, [HTTP_OK]);
|
|
298
|
+
var xml = Buffer.isBuffer(res.body) ? res.body.toString("utf8") :
|
|
299
|
+
typeof res.body === "string" ? res.body :
|
|
300
|
+
"";
|
|
301
|
+
var blocks = _extractBlocks(xml, "Container");
|
|
302
|
+
return blocks.map(function (block) {
|
|
303
|
+
return {
|
|
304
|
+
name: (_extractAll(block, "Name")[0] || "").trim(),
|
|
305
|
+
lastModified: (_extractAll(block, "Last-Modified")[0] || null),
|
|
306
|
+
etag: (_extractAll(block, "Etag")[0] || null),
|
|
307
|
+
leaseStatus: (_extractAll(block, "LeaseStatus")[0] || null),
|
|
308
|
+
leaseState: (_extractAll(block, "LeaseState")[0] || null),
|
|
309
|
+
publicAccess: (_extractAll(block, "PublicAccess")[0] || null),
|
|
310
|
+
};
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
async function setCorsRules(rules) {
|
|
315
|
+
if (!Array.isArray(rules)) {
|
|
316
|
+
throw _err("INVALID_CORS_RULE",
|
|
317
|
+
"azure-blob bucketOps: setCorsRules: rules must be an array", true);
|
|
318
|
+
}
|
|
319
|
+
rules.forEach(_validateCorsRule);
|
|
320
|
+
var xml = _buildCorsXml(rules);
|
|
321
|
+
var bodyBuf = Buffer.from(xml, "utf8");
|
|
322
|
+
var url = _internalUrl(endpoint + "/?restype=service&comp=properties", allowedProtocols);
|
|
323
|
+
var headers = {
|
|
324
|
+
"Content-Type": "application/xml",
|
|
325
|
+
"Content-Length": String(bodyBuf.length),
|
|
326
|
+
};
|
|
327
|
+
var signed = _sign("PUT", url, headers);
|
|
328
|
+
await _request("PUT", url, signed, bodyBuf, [HTTP_ACCEPTED]);
|
|
329
|
+
return { rulesApplied: rules.length };
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
return {
|
|
333
|
+
protocol: "azure-blob",
|
|
334
|
+
create: createContainer,
|
|
335
|
+
delete: deleteContainer,
|
|
336
|
+
list: listContainers,
|
|
337
|
+
setCorsRules: setCorsRules,
|
|
338
|
+
setLifecycle: function () {
|
|
339
|
+
throw _err("NOT_SUPPORTED",
|
|
340
|
+
"azure-blob bucketOps: setLifecycle is not implemented because " +
|
|
341
|
+
"Azure Storage lifecycle management policies live on Azure Resource " +
|
|
342
|
+
"Manager (management.azure.com) and require Azure AD bearer token " +
|
|
343
|
+
"auth, not Shared Key. Configure lifecycle via Terraform / Bicep / " +
|
|
344
|
+
"az CLI.", true);
|
|
345
|
+
},
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
module.exports = { create: create };
|