@crumbsdk/source-maps 0.0.1-rc.4
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/LICENSE +201 -0
- package/README.md +145 -0
- package/dist/cli.js +100 -0
- package/dist/upload.js +170 -0
- package/dist/validation.js +166 -0
- package/package.json +38 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
Apache License
|
|
2
|
+
Version 2.0, January 2004
|
|
3
|
+
http://www.apache.org/licenses/
|
|
4
|
+
|
|
5
|
+
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
|
6
|
+
|
|
7
|
+
1. Definitions.
|
|
8
|
+
|
|
9
|
+
"License" shall mean the terms and conditions for use, reproduction,
|
|
10
|
+
and distribution as defined by Sections 1 through 9 of this document.
|
|
11
|
+
|
|
12
|
+
"Licensor" shall mean the copyright owner or entity authorized by
|
|
13
|
+
the copyright owner that is granting the License.
|
|
14
|
+
|
|
15
|
+
"Legal Entity" shall mean the union of the acting entity and all
|
|
16
|
+
other entities that control, are controlled by, or are under common
|
|
17
|
+
control with that entity. For the purposes of this definition,
|
|
18
|
+
"control" means (i) the power, direct or indirect, to cause the
|
|
19
|
+
direction or management of such entity, whether by contract or
|
|
20
|
+
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
|
21
|
+
outstanding shares, or (iii) beneficial ownership of such entity.
|
|
22
|
+
|
|
23
|
+
"You" (or "Your") shall mean an individual or Legal Entity
|
|
24
|
+
exercising permissions granted by this License.
|
|
25
|
+
|
|
26
|
+
"Source" form shall mean the preferred form for making modifications,
|
|
27
|
+
including but not limited to software source code, documentation
|
|
28
|
+
source, and configuration files.
|
|
29
|
+
|
|
30
|
+
"Object" form shall mean any form resulting from mechanical
|
|
31
|
+
transformation or translation of a Source form, including but
|
|
32
|
+
not limited to compiled object code, generated documentation,
|
|
33
|
+
and conversions to other media types.
|
|
34
|
+
|
|
35
|
+
"Work" shall mean the work of authorship, whether in Source or
|
|
36
|
+
Object form, made available under the License, as indicated by a
|
|
37
|
+
copyright notice that is included in or attached to the work
|
|
38
|
+
(an example is provided in the Appendix below).
|
|
39
|
+
|
|
40
|
+
"Derivative Works" shall mean any work, whether in Source or Object
|
|
41
|
+
form, that is based on (or derived from) the Work and for which the
|
|
42
|
+
editorial revisions, annotations, elaborations, or other modifications
|
|
43
|
+
represent, as a whole, an original work of authorship. For the purposes
|
|
44
|
+
of this License, Derivative Works shall not include works that remain
|
|
45
|
+
separable from, or merely link (or bind by name) to the interfaces of,
|
|
46
|
+
the Work and Derivative Works thereof.
|
|
47
|
+
|
|
48
|
+
"Contribution" shall mean any work of authorship, including
|
|
49
|
+
the original version of the Work and any modifications or additions
|
|
50
|
+
to that Work or Derivative Works thereof, that is intentionally
|
|
51
|
+
submitted to Licensor for inclusion in the Work by the copyright owner
|
|
52
|
+
or by an individual or Legal Entity authorized to submit on behalf of
|
|
53
|
+
the copyright owner. For the purposes of this definition, "submitted"
|
|
54
|
+
means any form of electronic, verbal, or written communication sent
|
|
55
|
+
to the Licensor or its representatives, including but not limited to
|
|
56
|
+
communication on electronic mailing lists, source code control systems,
|
|
57
|
+
and issue tracking systems that are managed by, or on behalf of, the
|
|
58
|
+
Licensor for the purpose of discussing and improving the Work, but
|
|
59
|
+
excluding communication that is conspicuously marked or otherwise
|
|
60
|
+
designated in writing by the copyright owner as "Not a Contribution."
|
|
61
|
+
|
|
62
|
+
"Contributor" shall mean Licensor and any individual or Legal Entity
|
|
63
|
+
on behalf of whom a Contribution has been received by Licensor and
|
|
64
|
+
subsequently incorporated within the Work.
|
|
65
|
+
|
|
66
|
+
2. Grant of Copyright License. Subject to the terms and conditions of
|
|
67
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
68
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
69
|
+
copyright license to reproduce, prepare Derivative Works of,
|
|
70
|
+
publicly display, publicly perform, sublicense, and distribute the
|
|
71
|
+
Work and such Derivative Works in Source or Object form.
|
|
72
|
+
|
|
73
|
+
3. Grant of Patent License. Subject to the terms and conditions of
|
|
74
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
75
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
76
|
+
(except as stated in this section) patent license to make, have made,
|
|
77
|
+
use, offer to sell, sell, import, and otherwise transfer the Work,
|
|
78
|
+
where such license applies only to those patent claims licensable
|
|
79
|
+
by such Contributor that are necessarily infringed by their
|
|
80
|
+
Contribution(s) alone or by combination of their Contribution(s)
|
|
81
|
+
with the Work to which such Contribution(s) was submitted. If You
|
|
82
|
+
institute patent litigation against any entity (including a
|
|
83
|
+
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
|
84
|
+
or a Contribution incorporated within the Work constitutes direct
|
|
85
|
+
or contributory patent infringement, then any patent licenses
|
|
86
|
+
granted to You under this License for that Work shall terminate
|
|
87
|
+
as of the date such litigation is filed.
|
|
88
|
+
|
|
89
|
+
4. Redistribution. You may reproduce and distribute copies of the
|
|
90
|
+
Work or Derivative Works thereof in any medium, with or without
|
|
91
|
+
modifications, and in Source or Object form, provided that You
|
|
92
|
+
meet the following conditions:
|
|
93
|
+
|
|
94
|
+
(a) You must give any other recipients of the Work or
|
|
95
|
+
Derivative Works a copy of this License; and
|
|
96
|
+
|
|
97
|
+
(b) You must cause any modified files to carry prominent notices
|
|
98
|
+
stating that You changed the files; and
|
|
99
|
+
|
|
100
|
+
(c) You must retain, in the Source form of any Derivative Works
|
|
101
|
+
that You distribute, all copyright, patent, trademark, and
|
|
102
|
+
attribution notices from the Source form of the Work,
|
|
103
|
+
excluding those notices that do not pertain to any part of
|
|
104
|
+
the Derivative Works; and
|
|
105
|
+
|
|
106
|
+
(d) If the Work includes a "NOTICE" text file as part of its
|
|
107
|
+
distribution, then any Derivative Works that You distribute must
|
|
108
|
+
include a readable copy of the attribution notices contained
|
|
109
|
+
within such NOTICE file, excluding those notices that do not
|
|
110
|
+
pertain to any part of the Derivative Works, in at least one
|
|
111
|
+
of the following places: within a NOTICE text file distributed
|
|
112
|
+
as part of the Derivative Works; within the Source form or
|
|
113
|
+
documentation, if provided along with the Derivative Works; or,
|
|
114
|
+
within a display generated by the Derivative Works, if and
|
|
115
|
+
wherever such third-party notices normally appear. The contents
|
|
116
|
+
of the NOTICE file are for informational purposes only and
|
|
117
|
+
do not modify the License. You may add Your own attribution
|
|
118
|
+
notices within Derivative Works that You distribute, alongside
|
|
119
|
+
or as an addendum to the NOTICE text from the Work, provided
|
|
120
|
+
that such additional attribution notices cannot be construed
|
|
121
|
+
as modifying the License.
|
|
122
|
+
|
|
123
|
+
You may add Your own copyright statement to Your modifications and
|
|
124
|
+
may provide additional or different license terms and conditions
|
|
125
|
+
for use, reproduction, or distribution of Your modifications, or
|
|
126
|
+
for any such Derivative Works as a whole, provided Your use,
|
|
127
|
+
reproduction, and distribution of the Work otherwise complies with
|
|
128
|
+
the conditions stated in this License.
|
|
129
|
+
|
|
130
|
+
5. Submission of Contributions. Unless You explicitly state otherwise,
|
|
131
|
+
any Contribution intentionally submitted for inclusion in the Work
|
|
132
|
+
by You to the Licensor shall be under the terms and conditions of
|
|
133
|
+
this License, without any additional terms or conditions.
|
|
134
|
+
Notwithstanding the above, nothing herein shall supersede or modify
|
|
135
|
+
the terms of any separate license agreement you may have executed
|
|
136
|
+
with Licensor regarding such Contributions.
|
|
137
|
+
|
|
138
|
+
6. Trademarks. This License does not grant permission to use the trade
|
|
139
|
+
names, trademarks, service marks, or product names of the Licensor,
|
|
140
|
+
except as required for reasonable and customary use in describing the
|
|
141
|
+
origin of the Work and reproducing the content of the NOTICE file.
|
|
142
|
+
|
|
143
|
+
7. Disclaimer of Warranty. Unless required by applicable law or
|
|
144
|
+
agreed to in writing, Licensor provides the Work (and each
|
|
145
|
+
Contributor provides its Contributions) on an "AS IS" BASIS,
|
|
146
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
|
147
|
+
implied, including, without limitation, any warranties or conditions
|
|
148
|
+
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
|
149
|
+
PARTICULAR PURPOSE. You are solely responsible for determining the
|
|
150
|
+
appropriateness of using or redistributing the Work and assume any
|
|
151
|
+
risks associated with Your exercise of permissions under this License.
|
|
152
|
+
|
|
153
|
+
8. Limitation of Liability. In no event and under no legal theory,
|
|
154
|
+
whether in tort (including negligence), contract, or otherwise,
|
|
155
|
+
unless required by applicable law (such as deliberate and grossly
|
|
156
|
+
negligent acts) or agreed to in writing, shall any Contributor be
|
|
157
|
+
liable to You for damages, including any direct, indirect, special,
|
|
158
|
+
incidental, or consequential damages of any character arising as a
|
|
159
|
+
result of this License or out of the use or inability to use the
|
|
160
|
+
Work (including but not limited to damages for loss of goodwill,
|
|
161
|
+
work stoppage, computer failure or malfunction, or any and all
|
|
162
|
+
other commercial damages or losses), even if such Contributor
|
|
163
|
+
has been advised of the possibility of such damages.
|
|
164
|
+
|
|
165
|
+
9. Accepting Warranty or Additional Liability. While redistributing
|
|
166
|
+
the Work or Derivative Works thereof, You may choose to offer,
|
|
167
|
+
and charge a fee for, acceptance of support, warranty, indemnity,
|
|
168
|
+
or other liability obligations and/or rights consistent with this
|
|
169
|
+
License. However, in accepting such obligations, You may act only
|
|
170
|
+
on Your own behalf and on Your sole responsibility, not on behalf
|
|
171
|
+
of any other Contributor, and only if You agree to indemnify,
|
|
172
|
+
defend, and hold each Contributor harmless for any liability
|
|
173
|
+
incurred by, or claims asserted against, such Contributor by reason
|
|
174
|
+
of your accepting any such warranty or additional liability.
|
|
175
|
+
|
|
176
|
+
END OF TERMS AND CONDITIONS
|
|
177
|
+
|
|
178
|
+
APPENDIX: How to apply the Apache License to your work.
|
|
179
|
+
|
|
180
|
+
To apply the Apache License to your work, attach the following
|
|
181
|
+
boilerplate notice, with the fields enclosed by brackets "[]"
|
|
182
|
+
replaced with your own identifying information. (Don't include
|
|
183
|
+
the brackets!) The text should be enclosed in the appropriate
|
|
184
|
+
comment syntax for the file format. We also recommend that a
|
|
185
|
+
file or class name and description of purpose be included on the
|
|
186
|
+
same "printed page" as the copyright notice for easier
|
|
187
|
+
identification within third-party archives.
|
|
188
|
+
|
|
189
|
+
Copyright 2026 Crumb contributors
|
|
190
|
+
|
|
191
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
192
|
+
you may not use this file except in compliance with the License.
|
|
193
|
+
You may obtain a copy of the License at
|
|
194
|
+
|
|
195
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
196
|
+
|
|
197
|
+
Unless required by applicable law or agreed to in writing, software
|
|
198
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
199
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
200
|
+
See the License for the specific language governing permissions and
|
|
201
|
+
limitations under the License.
|
package/README.md
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
# @crumbsdk/source-maps
|
|
2
|
+
|
|
3
|
+
An explicit CI command for uploading a React Native release bundle and its
|
|
4
|
+
matching source map. Requires Node.js 22 or newer. It never runs inside your app
|
|
5
|
+
and has no runtime dependencies.
|
|
6
|
+
|
|
7
|
+
**rc.4 candidate:** confirm the exact package version is published before using
|
|
8
|
+
a registry installation; source preparation alone does not establish availability. Uploads require
|
|
9
|
+
a service implementing the [v1 upload contract](https://github.com/alexander126/crumb/blob/main/docs/contracts/source-map-upload.md)
|
|
10
|
+
and a dedicated project-scoped upload credential. A local dry-run does not
|
|
11
|
+
require either. Hosted compatibility and package publication are release gates.
|
|
12
|
+
|
|
13
|
+
## Run from this checkout
|
|
14
|
+
|
|
15
|
+
```sh
|
|
16
|
+
cd packages/source-maps
|
|
17
|
+
npm ci
|
|
18
|
+
npm run build
|
|
19
|
+
node dist/cli.js --help
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
After an approved package release, install its exact version as a development
|
|
23
|
+
dependency and use `crumb-source-maps` from a package script. For this candidate,
|
|
24
|
+
that version is `@crumbsdk/source-maps@0.0.1-rc.4`. Do not resolve an unversioned
|
|
25
|
+
package on each release build.
|
|
26
|
+
|
|
27
|
+
## Upload the exact release
|
|
28
|
+
|
|
29
|
+
Choose a unique immutable JavaScript bundle version for each exported build,
|
|
30
|
+
and supply the same value to the app's Crumb `release.bundleVersion`. Use the
|
|
31
|
+
actual native app version and build number embedded in the distributed binary.
|
|
32
|
+
Never use a branch name, mutable channel name, or an EAS runtime version as the
|
|
33
|
+
only bundle identity.
|
|
34
|
+
|
|
35
|
+
The example assumes the following variables are set by your build job:
|
|
36
|
+
`CRUMB_UPLOAD_ORIGIN`, `CRUMB_PLATFORM`, `APP_VERSION`, `NATIVE_BUILD`,
|
|
37
|
+
`JS_BUNDLE_VERSION`, `BUNDLE_FILE`, and `SOURCE_MAP_FILE`.
|
|
38
|
+
`CRUMB_SOURCE_MAP_TOKEN` must come from your CI secret store. It is a dedicated
|
|
39
|
+
upload credential; the SDK write key embedded in the app cannot upload maps.
|
|
40
|
+
|
|
41
|
+
```sh
|
|
42
|
+
node dist/cli.js upload \
|
|
43
|
+
--url "$CRUMB_UPLOAD_ORIGIN" \
|
|
44
|
+
--platform "$CRUMB_PLATFORM" \
|
|
45
|
+
--app-version "$APP_VERSION" \
|
|
46
|
+
--native-build "$NATIVE_BUILD" \
|
|
47
|
+
--bundle-version "$JS_BUNDLE_VERSION" \
|
|
48
|
+
--bundle "$BUNDLE_FILE" \
|
|
49
|
+
--source-map "$SOURCE_MAP_FILE"
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Use `ios` or `android`, including for React Native. All release fields are
|
|
53
|
+
required. Identity values start with a letter or digit and contain only letters,
|
|
54
|
+
digits, dot, underscore, plus or hyphen. App version and native build are at most
|
|
55
|
+
64 characters; bundle version is at most 128.
|
|
56
|
+
|
|
57
|
+
Add `--dry-run` to validate and hash files without network access or reading a
|
|
58
|
+
credential. Add `--expected-bundle-sha256` and `--expected-source-map-sha256` to
|
|
59
|
+
verify checksums saved by an earlier build step. Supply lowercase hexadecimal
|
|
60
|
+
SHA-256 values. The command rejects a mismatch before making any request.
|
|
61
|
+
|
|
62
|
+
Alternatively, supply `--token-file /path/to/ci-secret`. A single trailing newline
|
|
63
|
+
is accepted. Do not also set `CRUMB_SOURCE_MAP_TOKEN`. No command-line token flag
|
|
64
|
+
is supported, no credentials are discovered automatically, and the tool never
|
|
65
|
+
prints the token, input paths, bundle, map, signed URLs or server response body.
|
|
66
|
+
Keep shell tracing disabled for secret-handling steps. Never put upload secrets
|
|
67
|
+
in app configuration or an `EXPO_PUBLIC_*` variable.
|
|
68
|
+
|
|
69
|
+
Only HTTPS is accepted. The API origin must not contain a path, query, fragment
|
|
70
|
+
or credentials. Redirects are rejected, and authorization is never forwarded to
|
|
71
|
+
artifact upload locations. `--allow-localhost` permits HTTP only on localhost,
|
|
72
|
+
127.0.0.1 or ::1 for local fixture tests.
|
|
73
|
+
|
|
74
|
+
## Results and retries
|
|
75
|
+
|
|
76
|
+
Except for `--help`, stdout/stderr contain one JSON result. Success exits 0;
|
|
77
|
+
failure exits 1. Success is written to stdout; failure to stderr.
|
|
78
|
+
|
|
79
|
+
```json
|
|
80
|
+
{"ok":true,"status":"verified","upload_id":"00000000-0000-4000-8000-000000000001","manifest_sha256":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
`verified` means the service verified and stored both artifacts. It does not
|
|
84
|
+
mean any crash has been symbolicated. Dry-run returns `status: "dry_run"`, the
|
|
85
|
+
manifest digest and both byte counts; it makes no storage claim.
|
|
86
|
+
|
|
87
|
+
```json
|
|
88
|
+
{"ok":false,"error":{"code":"release_conflict","message":"This release identity already has different artifacts. Use the matching build or a new bundle version."}}
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Rerun the **same command with unchanged artifacts** after a timeout or interrupted
|
|
92
|
+
upload. The service can refresh expired upload locations, and exact duplicates
|
|
93
|
+
return the same upload ID. The CLI does not automatically retry. Each request
|
|
94
|
+
has a 60-second deadline. A different artifact pair under the same release is a
|
|
95
|
+
conflict; use the correct build or assign a new bundle version and rebuild.
|
|
96
|
+
|
|
97
|
+
Common failure codes: `invalid_arguments`, `invalid_release`, `invalid_platform`,
|
|
98
|
+
`file_unavailable`, `invalid_size`, `invalid_source_map`, `checksum_mismatch`,
|
|
99
|
+
`missing_token`, `ambiguous_token`, `invalid_token`, `unauthorized`,
|
|
100
|
+
`release_conflict`, `artifact_rejected`, `artifact_too_large`, `rate_limited`,
|
|
101
|
+
`artifact_upload_failed`, `network_error`, and `invalid_response`.
|
|
102
|
+
|
|
103
|
+
## Build artifacts and limits
|
|
104
|
+
|
|
105
|
+
Supply the final bundle shipped to users and the final corresponding v3 map.
|
|
106
|
+
For Hermes, this means the bytecode bundle and the map composed through Hermes,
|
|
107
|
+
not only the intermediate Metro packager map. Never rebuild just to obtain a
|
|
108
|
+
missing map: archive both outputs of the original build together.
|
|
109
|
+
|
|
110
|
+
Both files must be nonempty regular files, at most 25 MiB each. Standard and
|
|
111
|
+
inline indexed maps are supported; external section URLs are rejected. Map
|
|
112
|
+
validation checks JSON structure, not whether every mapping correctly describes
|
|
113
|
+
the compiled bundle. Hashes establish byte identity, not semantic correctness.
|
|
114
|
+
Confirm a synthetic crash resolves to its known source location before release.
|
|
115
|
+
The tool never executes a bundle or downloads source URLs.
|
|
116
|
+
|
|
117
|
+
Source maps may contain original source code in `sourcesContent`. The matching
|
|
118
|
+
bundle and map are deliberately uploaded as private release artifacts. Review
|
|
119
|
+
your build outputs, use a narrowly scoped CI credential, and keep these files
|
|
120
|
+
out of public build artifacts and the app's published assets when unnecessary.
|
|
121
|
+
|
|
122
|
+
See the [Metro, bare React Native and Expo/EAS guide](https://github.com/alexander126/crumb/blob/main/docs/source-maps.md)
|
|
123
|
+
for release-pipeline examples.
|
|
124
|
+
|
|
125
|
+
## Package checks and publication
|
|
126
|
+
|
|
127
|
+
```sh
|
|
128
|
+
npm ci
|
|
129
|
+
npm run typecheck
|
|
130
|
+
npm test
|
|
131
|
+
npm run pack:check
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
The packaging check installs the produced tarball offline, invokes its executable,
|
|
135
|
+
and performs a dry-run. The allowlist contains only compiled CLI files, this
|
|
136
|
+
README, the license and package metadata.
|
|
137
|
+
|
|
138
|
+
The package version must match the root `VERSION`; when bumping it, update this
|
|
139
|
+
package and its lockfile together. The manual `source-maps-npm-publish.yml`
|
|
140
|
+
workflow requires the immutable native release tag `<version>` and the explicit
|
|
141
|
+
confirmation `publish @crumbsdk/source-maps@<version>`. Prereleases use `next`;
|
|
142
|
+
stable releases use `latest`. The workflow runs checks before publishing using
|
|
143
|
+
npm trusted publishing. Registry ownership/trusted-publisher setup and the first
|
|
144
|
+
publication require an authorized release owner; they are not performed by CI
|
|
145
|
+
checks or this CLI.
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { parseArgs } from "node:util";
|
|
3
|
+
import { prepareUpload, upload } from "./upload.js";
|
|
4
|
+
import { readBoundedFile, UploadError } from "./validation.js";
|
|
5
|
+
const HELP = `crumb-source-maps upload --platform ios|android --app-version VERSION
|
|
6
|
+
--native-build BUILD --bundle-version ID --bundle FILE --source-map FILE
|
|
7
|
+
--url HTTPS_API_ORIGIN [--token-file FILE]
|
|
8
|
+
[--expected-bundle-sha256 HASH] [--expected-source-map-sha256 HASH]
|
|
9
|
+
[--dry-run] [--allow-localhost]
|
|
10
|
+
|
|
11
|
+
Credentials: CRUMB_SOURCE_MAP_TOKEN or --token-file, never a command-line token.
|
|
12
|
+
--dry-run validates and hashes inputs without reading credentials or using network.
|
|
13
|
+
Inputs: final matching build artifacts, at most 25 MiB each. Node.js 22 or newer.
|
|
14
|
+
Successful upload means verified storage, not successful crash symbolication.
|
|
15
|
+
Outputs are JSON except --help. Exit code 0 is success; 1 is a safe failure.
|
|
16
|
+
`;
|
|
17
|
+
async function main() {
|
|
18
|
+
let parsed;
|
|
19
|
+
try {
|
|
20
|
+
parsed = parseArgs({
|
|
21
|
+
allowPositionals: true,
|
|
22
|
+
strict: true,
|
|
23
|
+
tokens: true,
|
|
24
|
+
options: {
|
|
25
|
+
help: { type: "boolean", short: "h" },
|
|
26
|
+
"dry-run": { type: "boolean" },
|
|
27
|
+
"allow-localhost": { type: "boolean" },
|
|
28
|
+
url: { type: "string" },
|
|
29
|
+
platform: { type: "string" },
|
|
30
|
+
"app-version": { type: "string" },
|
|
31
|
+
"native-build": { type: "string" },
|
|
32
|
+
"bundle-version": { type: "string" },
|
|
33
|
+
bundle: { type: "string" },
|
|
34
|
+
"source-map": { type: "string" },
|
|
35
|
+
"token-file": { type: "string" },
|
|
36
|
+
"expected-bundle-sha256": { type: "string" },
|
|
37
|
+
"expected-source-map-sha256": { type: "string" },
|
|
38
|
+
},
|
|
39
|
+
});
|
|
40
|
+
const seen = new Set();
|
|
41
|
+
for (const token of parsed.tokens) {
|
|
42
|
+
if (token.kind !== "option")
|
|
43
|
+
continue;
|
|
44
|
+
if (seen.has(token.name))
|
|
45
|
+
throw new Error("duplicate option");
|
|
46
|
+
seen.add(token.name);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
throw new UploadError("invalid_arguments", "Invalid command options. Use --help; credentials are accepted only through environment or a secret file.");
|
|
51
|
+
}
|
|
52
|
+
const { values, positionals } = parsed;
|
|
53
|
+
if (values.help) {
|
|
54
|
+
process.stdout.write(HELP);
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
if (positionals.length !== 1 ||
|
|
58
|
+
positionals[0] !== "upload" ||
|
|
59
|
+
!values.bundle ||
|
|
60
|
+
!values["source-map"])
|
|
61
|
+
throw new UploadError("invalid_arguments", "Use upload with release identity, --bundle and --source-map. See --help.");
|
|
62
|
+
const prepared = await prepareUpload({
|
|
63
|
+
platform: values.platform,
|
|
64
|
+
appVersion: values["app-version"],
|
|
65
|
+
nativeBuild: values["native-build"],
|
|
66
|
+
bundleVersion: values["bundle-version"],
|
|
67
|
+
bundle: values.bundle,
|
|
68
|
+
sourceMap: values["source-map"],
|
|
69
|
+
expectedBundleSha256: values["expected-bundle-sha256"],
|
|
70
|
+
expectedSourceMapSha256: values["expected-source-map-sha256"],
|
|
71
|
+
});
|
|
72
|
+
if (values["dry-run"]) {
|
|
73
|
+
process.stdout.write(`${JSON.stringify({ ok: true, status: "dry_run", manifest_sha256: prepared.manifestSha256, bundle_bytes: prepared.bundle.length, source_map_bytes: prepared.sourceMap.length })}\n`);
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
if (!values.url)
|
|
77
|
+
throw new UploadError("invalid_arguments", "Supply the HTTPS upload API origin with --url.");
|
|
78
|
+
if (values["token-file"] && process.env.CRUMB_SOURCE_MAP_TOKEN)
|
|
79
|
+
throw new UploadError("ambiguous_token", "Choose one credential source: CRUMB_SOURCE_MAP_TOKEN or --token-file.");
|
|
80
|
+
const token = values["token-file"]
|
|
81
|
+
? (await readBoundedFile(values["token-file"], 4098))
|
|
82
|
+
.toString("utf8")
|
|
83
|
+
.replace(/\r?\n$/, "")
|
|
84
|
+
: process.env.CRUMB_SOURCE_MAP_TOKEN;
|
|
85
|
+
if (!token)
|
|
86
|
+
throw new UploadError("missing_token", "Set CRUMB_SOURCE_MAP_TOKEN or supply --token-file with a dedicated upload credential.");
|
|
87
|
+
const result = await upload(prepared, {
|
|
88
|
+
url: values.url,
|
|
89
|
+
token,
|
|
90
|
+
allowLocalhost: values["allow-localhost"] === true,
|
|
91
|
+
});
|
|
92
|
+
process.stdout.write(`${JSON.stringify(result)}\n`);
|
|
93
|
+
}
|
|
94
|
+
main().catch((error) => {
|
|
95
|
+
const safe = error instanceof UploadError
|
|
96
|
+
? error
|
|
97
|
+
: new UploadError("upload_failed", "The upload could not complete. Check the build inputs and retry.");
|
|
98
|
+
process.stderr.write(`${JSON.stringify({ ok: false, error: { code: safe.code, message: safe.message } })}\n`);
|
|
99
|
+
process.exitCode = 1;
|
|
100
|
+
});
|
package/dist/upload.js
ADDED
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import { canonicalJson, identity, MAX_ARTIFACT_BYTES, MAX_RESPONSE_BYTES, readBoundedFile, record, safeUrl, sha256, UploadError, validateSourceMap, verifyExpectedHash, } from "./validation.js";
|
|
2
|
+
export async function prepareUpload(options) {
|
|
3
|
+
if (options.platform !== "ios" && options.platform !== "android")
|
|
4
|
+
throw new UploadError("invalid_platform", "The platform must be ios or android, including for React Native.");
|
|
5
|
+
const release = {
|
|
6
|
+
platform: options.platform,
|
|
7
|
+
app_version: identity(options.appVersion, 64),
|
|
8
|
+
native_build: identity(options.nativeBuild, 64),
|
|
9
|
+
js_bundle_version: identity(options.bundleVersion, 128),
|
|
10
|
+
};
|
|
11
|
+
const bundle = await readBoundedFile(options.bundle, MAX_ARTIFACT_BYTES);
|
|
12
|
+
const sourceMap = await readBoundedFile(options.sourceMap, MAX_ARTIFACT_BYTES);
|
|
13
|
+
validateSourceMap(sourceMap);
|
|
14
|
+
const manifest = {
|
|
15
|
+
schema_version: "1.0",
|
|
16
|
+
release: { ...release, platform: options.platform },
|
|
17
|
+
bundle: {
|
|
18
|
+
byte_size: bundle.length,
|
|
19
|
+
sha256: verifyExpectedHash(bundle, options.expectedBundleSha256),
|
|
20
|
+
},
|
|
21
|
+
source_map: {
|
|
22
|
+
byte_size: sourceMap.length,
|
|
23
|
+
sha256: verifyExpectedHash(sourceMap, options.expectedSourceMapSha256),
|
|
24
|
+
},
|
|
25
|
+
};
|
|
26
|
+
return {
|
|
27
|
+
manifest,
|
|
28
|
+
manifestSha256: sha256(canonicalJson(manifest)),
|
|
29
|
+
bundle,
|
|
30
|
+
sourceMap,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
const UUID = /^[a-f0-9]{8}-[a-f0-9]{4}-[1-5][a-f0-9]{3}-[89ab][a-f0-9]{3}-[a-f0-9]{12}$/i;
|
|
34
|
+
export async function upload(prepared, options) {
|
|
35
|
+
const base = safeUrl(options.url, options.allowLocalhost === true, true);
|
|
36
|
+
if (!/^[\x21-\x7e]{16,4096}$/.test(options.token))
|
|
37
|
+
throw new UploadError("invalid_token", "Supply a valid source-map upload credential using the environment or a secret file.");
|
|
38
|
+
const request = options.fetch ?? fetch;
|
|
39
|
+
const timeoutMs = options.timeoutMs ?? 60_000;
|
|
40
|
+
async function send(url, init) {
|
|
41
|
+
try {
|
|
42
|
+
return await request(url, {
|
|
43
|
+
...init,
|
|
44
|
+
redirect: "error",
|
|
45
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
throw new UploadError("network_error", "The upload request did not complete. Check connectivity and rerun the same command to resume safely.");
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
async function api(path, body, operation) {
|
|
53
|
+
const response = await send(new URL(path, base), {
|
|
54
|
+
method: "POST",
|
|
55
|
+
headers: {
|
|
56
|
+
authorization: `Bearer ${options.token}`,
|
|
57
|
+
"content-type": "application/json",
|
|
58
|
+
"idempotency-key": `source-maps:${prepared.manifestSha256}:${operation}`,
|
|
59
|
+
},
|
|
60
|
+
body: canonicalJson(body),
|
|
61
|
+
});
|
|
62
|
+
if (!response.ok) {
|
|
63
|
+
await response.body?.cancel();
|
|
64
|
+
throw httpError(response.status);
|
|
65
|
+
}
|
|
66
|
+
const result = await readJson(response);
|
|
67
|
+
if (!record(result) ||
|
|
68
|
+
result.manifest_sha256 !== prepared.manifestSha256 ||
|
|
69
|
+
typeof result.upload_id !== "string" ||
|
|
70
|
+
!UUID.test(result.upload_id))
|
|
71
|
+
throw protocolError();
|
|
72
|
+
return result;
|
|
73
|
+
}
|
|
74
|
+
const initialized = await api("/source-maps/v1/uploads/init", { manifest: prepared.manifest }, "init");
|
|
75
|
+
if (!record(initialized) || typeof initialized.upload_id !== "string")
|
|
76
|
+
throw protocolError();
|
|
77
|
+
const uploadId = initialized.upload_id;
|
|
78
|
+
if (initialized.status === "verified")
|
|
79
|
+
return {
|
|
80
|
+
ok: true,
|
|
81
|
+
status: "verified",
|
|
82
|
+
upload_id: uploadId,
|
|
83
|
+
manifest_sha256: prepared.manifestSha256,
|
|
84
|
+
};
|
|
85
|
+
if (initialized.status !== "initialized" ||
|
|
86
|
+
!Array.isArray(initialized.artifacts) ||
|
|
87
|
+
initialized.artifacts.length !== 2)
|
|
88
|
+
throw protocolError();
|
|
89
|
+
const targets = [];
|
|
90
|
+
for (const target of initialized.artifacts) {
|
|
91
|
+
if (!record(target) ||
|
|
92
|
+
(target.kind !== "bundle" && target.kind !== "source_map") ||
|
|
93
|
+
target.method !== "PUT" ||
|
|
94
|
+
typeof target.url !== "string" ||
|
|
95
|
+
!record(target.headers) ||
|
|
96
|
+
Object.keys(target.headers).length !== 1 ||
|
|
97
|
+
target.headers["content-type"] !== "application/octet-stream")
|
|
98
|
+
throw protocolError();
|
|
99
|
+
safeUrl(target.url, options.allowLocalhost === true);
|
|
100
|
+
if (targets.some((existing) => existing.kind === target.kind))
|
|
101
|
+
throw protocolError();
|
|
102
|
+
targets.push({ kind: target.kind, url: target.url });
|
|
103
|
+
}
|
|
104
|
+
// Validate every destination before sending either private artifact. Credentials
|
|
105
|
+
// are sent only to the API, never to a signed upload target or redirect.
|
|
106
|
+
for (const target of targets) {
|
|
107
|
+
const bytes = target.kind === "bundle" ? prepared.bundle : prepared.sourceMap;
|
|
108
|
+
const response = await send(target.url, {
|
|
109
|
+
method: "PUT",
|
|
110
|
+
headers: { "content-type": "application/octet-stream" },
|
|
111
|
+
body: new Uint8Array(bytes),
|
|
112
|
+
});
|
|
113
|
+
await response.body?.cancel();
|
|
114
|
+
if (!response.ok)
|
|
115
|
+
throw new UploadError("artifact_upload_failed", "An artifact upload failed. Rerun the same command to obtain fresh upload locations and resume.");
|
|
116
|
+
}
|
|
117
|
+
const completed = await api(`/source-maps/v1/uploads/${uploadId}/complete`, {}, "complete");
|
|
118
|
+
if (!record(completed) ||
|
|
119
|
+
completed.status !== "verified" ||
|
|
120
|
+
completed.upload_id !== uploadId)
|
|
121
|
+
throw protocolError();
|
|
122
|
+
return {
|
|
123
|
+
ok: true,
|
|
124
|
+
status: "verified",
|
|
125
|
+
upload_id: uploadId,
|
|
126
|
+
manifest_sha256: prepared.manifestSha256,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
async function readJson(response) {
|
|
130
|
+
const reader = response.body?.getReader();
|
|
131
|
+
if (!reader)
|
|
132
|
+
throw protocolError();
|
|
133
|
+
const chunks = [];
|
|
134
|
+
let size = 0;
|
|
135
|
+
try {
|
|
136
|
+
while (true) {
|
|
137
|
+
const { done, value } = await reader.read();
|
|
138
|
+
if (done)
|
|
139
|
+
break;
|
|
140
|
+
size += value.byteLength;
|
|
141
|
+
if (size > MAX_RESPONSE_BYTES)
|
|
142
|
+
throw protocolError();
|
|
143
|
+
chunks.push(value);
|
|
144
|
+
}
|
|
145
|
+
return JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(Buffer.concat(chunks)));
|
|
146
|
+
}
|
|
147
|
+
catch {
|
|
148
|
+
throw protocolError();
|
|
149
|
+
}
|
|
150
|
+
finally {
|
|
151
|
+
await reader.cancel().catch(() => undefined);
|
|
152
|
+
reader.releaseLock();
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
function protocolError() {
|
|
156
|
+
return new UploadError("invalid_response", "The upload service returned an invalid response. No verification is assumed; retry with a compatible service.");
|
|
157
|
+
}
|
|
158
|
+
function httpError(status) {
|
|
159
|
+
if (status === 401 || status === 403)
|
|
160
|
+
return new UploadError("unauthorized", "The upload credential is invalid, expired or not permitted for this project.");
|
|
161
|
+
if (status === 409)
|
|
162
|
+
return new UploadError("release_conflict", "This release identity already has different artifacts. Use the matching build or a new bundle version.");
|
|
163
|
+
if (status === 413)
|
|
164
|
+
return new UploadError("artifact_too_large", "The release artifacts exceed the service limits.");
|
|
165
|
+
if (status === 422)
|
|
166
|
+
return new UploadError("artifact_rejected", "The release metadata or artifact verification failed. Check the build identity and checksums.");
|
|
167
|
+
if (status === 429)
|
|
168
|
+
return new UploadError("rate_limited", "Upload requests are rate limited. Wait before retrying the same command.");
|
|
169
|
+
return new UploadError("service_unavailable", "The upload service could not finish the request. Retry the same command later.");
|
|
170
|
+
}
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { open } from "node:fs/promises";
|
|
3
|
+
import { constants } from "node:fs";
|
|
4
|
+
export const MAX_ARTIFACT_BYTES = 25 * 1024 * 1024;
|
|
5
|
+
export const MAX_RESPONSE_BYTES = 64 * 1024;
|
|
6
|
+
export class UploadError extends Error {
|
|
7
|
+
code;
|
|
8
|
+
constructor(code, message) {
|
|
9
|
+
super(message);
|
|
10
|
+
this.code = code;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
export function record(value) {
|
|
14
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
15
|
+
}
|
|
16
|
+
export function sha256(value) {
|
|
17
|
+
return createHash("sha256").update(value).digest("hex");
|
|
18
|
+
}
|
|
19
|
+
export function canonicalJson(value) {
|
|
20
|
+
if (Array.isArray(value))
|
|
21
|
+
return `[${value.map(canonicalJson).join(",")}]`;
|
|
22
|
+
if (record(value))
|
|
23
|
+
return `{${Object.keys(value)
|
|
24
|
+
.sort()
|
|
25
|
+
.map((key) => `${JSON.stringify(key)}:${canonicalJson(value[key])}`)
|
|
26
|
+
.join(",")}}`;
|
|
27
|
+
const result = JSON.stringify(value);
|
|
28
|
+
if (result === undefined)
|
|
29
|
+
throw new UploadError("invalid_manifest", "The release manifest is invalid.");
|
|
30
|
+
return result;
|
|
31
|
+
}
|
|
32
|
+
export function identity(value, maximum) {
|
|
33
|
+
if (!value ||
|
|
34
|
+
value.length > maximum ||
|
|
35
|
+
!/^[A-Za-z0-9][A-Za-z0-9._+-]*$/.test(value)) {
|
|
36
|
+
throw new UploadError("invalid_release", "Supply platform, app version, native build and a unique bundle version using letters, digits, dot, underscore, plus or hyphen.");
|
|
37
|
+
}
|
|
38
|
+
return value;
|
|
39
|
+
}
|
|
40
|
+
/** Read only a bounded regular file, even if it changes or grows after stat. */
|
|
41
|
+
export async function readBoundedFile(path, maximum) {
|
|
42
|
+
let file;
|
|
43
|
+
try {
|
|
44
|
+
file = await open(path, constants.O_RDONLY | constants.O_NONBLOCK);
|
|
45
|
+
const info = await file.stat();
|
|
46
|
+
if (!info.isFile())
|
|
47
|
+
throw new UploadError("invalid_file", "Upload inputs must be regular files.");
|
|
48
|
+
if (info.size === 0 || info.size > maximum)
|
|
49
|
+
throw new UploadError("invalid_size", "An input is empty or exceeds its size limit.");
|
|
50
|
+
const buffer = Buffer.alloc(Math.min(info.size + 1, maximum + 1));
|
|
51
|
+
let offset = 0;
|
|
52
|
+
while (offset < buffer.length) {
|
|
53
|
+
const { bytesRead } = await file.read(buffer, offset, buffer.length - offset, null);
|
|
54
|
+
if (bytesRead === 0)
|
|
55
|
+
break;
|
|
56
|
+
offset += bytesRead;
|
|
57
|
+
}
|
|
58
|
+
if (offset !== info.size)
|
|
59
|
+
throw new UploadError("file_changed", "An input changed while being read. Finish the build and retry.");
|
|
60
|
+
return buffer.subarray(0, offset);
|
|
61
|
+
}
|
|
62
|
+
catch (error) {
|
|
63
|
+
if (error instanceof UploadError)
|
|
64
|
+
throw error;
|
|
65
|
+
throw new UploadError("file_unavailable", "An input file could not be read. Check its path and permissions.");
|
|
66
|
+
}
|
|
67
|
+
finally {
|
|
68
|
+
await file?.close();
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
export function verifyExpectedHash(bytes, expected) {
|
|
72
|
+
const actual = sha256(bytes);
|
|
73
|
+
if (expected !== undefined &&
|
|
74
|
+
(!/^[a-f0-9]{64}$/.test(expected) || expected !== actual)) {
|
|
75
|
+
throw new UploadError("checksum_mismatch", "An input does not match its expected SHA-256. Use the artifacts from the same release build.");
|
|
76
|
+
}
|
|
77
|
+
return actual;
|
|
78
|
+
}
|
|
79
|
+
/** Check the v3 JSON structure without executing bundles or resolving source URLs. */
|
|
80
|
+
export function validateSourceMap(bytes) {
|
|
81
|
+
let value;
|
|
82
|
+
try {
|
|
83
|
+
value = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes));
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
throw new UploadError("invalid_source_map", "The source map must be valid UTF-8 JSON.");
|
|
87
|
+
}
|
|
88
|
+
let count = 0;
|
|
89
|
+
function validate(map, depth) {
|
|
90
|
+
if (++count > 1024 || depth > 8 || !record(map) || map.version !== 3)
|
|
91
|
+
throw invalidMap();
|
|
92
|
+
if (map.file !== undefined && typeof map.file !== "string")
|
|
93
|
+
throw invalidMap();
|
|
94
|
+
if (map.sourceRoot !== undefined && typeof map.sourceRoot !== "string")
|
|
95
|
+
throw invalidMap();
|
|
96
|
+
if (map.sections !== undefined) {
|
|
97
|
+
if (!Array.isArray(map.sections) ||
|
|
98
|
+
map.sections.length === 0 ||
|
|
99
|
+
map.mappings !== undefined ||
|
|
100
|
+
map.sources !== undefined)
|
|
101
|
+
throw invalidMap();
|
|
102
|
+
let previousLine = -1;
|
|
103
|
+
let previousColumn = -1;
|
|
104
|
+
for (const section of map.sections) {
|
|
105
|
+
if (!record(section) ||
|
|
106
|
+
section.url !== undefined ||
|
|
107
|
+
!record(section.offset))
|
|
108
|
+
throw invalidMap();
|
|
109
|
+
const { line, column } = section.offset;
|
|
110
|
+
if (typeof line !== "number" ||
|
|
111
|
+
typeof column !== "number" ||
|
|
112
|
+
!Number.isSafeInteger(line) ||
|
|
113
|
+
!Number.isSafeInteger(column) ||
|
|
114
|
+
line < 0 ||
|
|
115
|
+
column < 0)
|
|
116
|
+
throw invalidMap();
|
|
117
|
+
if (line < previousLine ||
|
|
118
|
+
(line === previousLine && column <= previousColumn))
|
|
119
|
+
throw invalidMap();
|
|
120
|
+
previousLine = line;
|
|
121
|
+
previousColumn = column;
|
|
122
|
+
validate(section.map, depth + 1);
|
|
123
|
+
}
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
if (typeof map.mappings !== "string" ||
|
|
127
|
+
!/^[A-Za-z0-9+/;,]*$/.test(map.mappings) ||
|
|
128
|
+
!Array.isArray(map.sources) ||
|
|
129
|
+
!map.sources.every((source) => typeof source === "string"))
|
|
130
|
+
throw invalidMap();
|
|
131
|
+
if (map.names !== undefined &&
|
|
132
|
+
(!Array.isArray(map.names) ||
|
|
133
|
+
!map.names.every((name) => typeof name === "string")))
|
|
134
|
+
throw invalidMap();
|
|
135
|
+
if (map.sourcesContent !== undefined &&
|
|
136
|
+
(!Array.isArray(map.sourcesContent) ||
|
|
137
|
+
map.sourcesContent.length !== map.sources.length ||
|
|
138
|
+
!map.sourcesContent.every((source) => source === null || typeof source === "string")))
|
|
139
|
+
throw invalidMap();
|
|
140
|
+
}
|
|
141
|
+
validate(value, 0);
|
|
142
|
+
}
|
|
143
|
+
function invalidMap() {
|
|
144
|
+
return new UploadError("invalid_source_map", "Use a version 3 source map with inline sections only. Check its sources, mappings and offsets.");
|
|
145
|
+
}
|
|
146
|
+
export function safeUrl(input, allowLocal, base = false) {
|
|
147
|
+
let url;
|
|
148
|
+
try {
|
|
149
|
+
url = new URL(input);
|
|
150
|
+
}
|
|
151
|
+
catch {
|
|
152
|
+
throw invalidUrl();
|
|
153
|
+
}
|
|
154
|
+
const loopback = ["localhost", "127.0.0.1", "[::1]"].includes(url.hostname);
|
|
155
|
+
if ((url.protocol !== "https:" &&
|
|
156
|
+
!(allowLocal && loopback && url.protocol === "http:")) ||
|
|
157
|
+
url.username ||
|
|
158
|
+
url.password ||
|
|
159
|
+
url.hash ||
|
|
160
|
+
(base && (url.search || url.pathname !== "/")))
|
|
161
|
+
throw invalidUrl();
|
|
162
|
+
return url;
|
|
163
|
+
}
|
|
164
|
+
function invalidUrl() {
|
|
165
|
+
return new UploadError("invalid_url", "Use an HTTPS API origin without credentials, path, query or fragment. HTTP loopback requires --allow-localhost.");
|
|
166
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@crumbsdk/source-maps",
|
|
3
|
+
"version": "0.0.1-rc.4",
|
|
4
|
+
"description": "Explicit, CI-safe bundle and source-map uploads for Crumb",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"crumb-source-maps": "dist/cli.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"dist",
|
|
11
|
+
"README.md",
|
|
12
|
+
"LICENSE"
|
|
13
|
+
],
|
|
14
|
+
"engines": {
|
|
15
|
+
"node": ">=22"
|
|
16
|
+
},
|
|
17
|
+
"license": "Apache-2.0",
|
|
18
|
+
"repository": {
|
|
19
|
+
"type": "git",
|
|
20
|
+
"url": "git+https://github.com/alexander126/crumb.git",
|
|
21
|
+
"directory": "packages/source-maps"
|
|
22
|
+
},
|
|
23
|
+
"publishConfig": {
|
|
24
|
+
"access": "public",
|
|
25
|
+
"registry": "https://registry.npmjs.org/"
|
|
26
|
+
},
|
|
27
|
+
"scripts": {
|
|
28
|
+
"build": "tsc && node -e \"require('node:fs').chmodSync('dist/cli.js', 0o755)\"",
|
|
29
|
+
"typecheck": "tsc --noEmit",
|
|
30
|
+
"test": "npm run build && node --test test/*.test.mjs",
|
|
31
|
+
"pack:check": "npm run build && node test/pack-check.mjs",
|
|
32
|
+
"prepack": "npm run build"
|
|
33
|
+
},
|
|
34
|
+
"devDependencies": {
|
|
35
|
+
"@types/node": "26.2.0",
|
|
36
|
+
"typescript": "7.0.2"
|
|
37
|
+
}
|
|
38
|
+
}
|