mirror of
https://github.com/expo/eas-cli.git
synced 2026-11-13 20:28:08 -10:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6fa46c98f5 | ||
|
|
e507735166 |
No files matched your search
@@ -1,5 +0,0 @@
|
||||
---
|
||||
'eas-cli': patch
|
||||
---
|
||||
|
||||
Regenerate graphql code
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
'@expo/build-tools': patch
|
||||
---
|
||||
|
||||
Default Android emulators to 4 GiB of RAM to reduce ADB disconnects during large transfers. Retain the 2 GiB default for API 34 and below outside interactive device sessions, and preserve explicit emulator configuration overrides.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
'@expo/eas-json': minor
|
||||
---
|
||||
|
||||
Add `permissions.account.role` to build profiles to request a role for the build's `EXPO_TOKEN`.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
'@expo/build-tools': patch
|
||||
---
|
||||
|
||||
Retry transient failures when reporting that a device run session has started.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
'eas-cli': patch
|
||||
---
|
||||
|
||||
Read the app config fewer times in `eas build` and other project commands. An iOS build now runs `expo config` 2 times instead of 6.
|
||||
@@ -1,6 +0,0 @@
|
||||
---
|
||||
'@expo/build-tools': patch
|
||||
'@expo/steps': patch
|
||||
---
|
||||
|
||||
Stop printing workflow hook parsing messages in build logs: native builds showed them under an "Unknown build phase" group, and custom builds warned about unknown hook keys and hooks on steps that never run.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
'@expo/worker': patch
|
||||
---
|
||||
|
||||
Retry Node.js installation without the internal Node.js cache mirror when the first attempt fails, while preserving other configured mirrors.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
'eas-cli': patch
|
||||
---
|
||||
|
||||
Share one GraphQL client between the project ID check and the command, so commands send one fewer app query.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
'@expo/build-tools': minor
|
||||
---
|
||||
|
||||
Unify signed URL uploading to a single implementation and use it everywhere
|
||||
@@ -1,6 +0,0 @@
|
||||
---
|
||||
'@expo/eas-build-job': major
|
||||
'@expo/build-tools': major
|
||||
---
|
||||
|
||||
Remove legacy artifact type exports and route workflow uploads through workflow storage without file-type categories. Preserve workflow uploads and Maestro reports.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
'@expo/eas-build-job': patch
|
||||
---
|
||||
|
||||
Add types and validation for documented GitHub workflow context fields.
|
||||
@@ -215,7 +215,7 @@ if: ${{ steps.previous_step.outputs.success == 'true' && env.ENVIRONMENT == 'pro
|
||||
Artifacts tracked as `ArtifactToUpload`:
|
||||
|
||||
- Managed artifacts (APK, IPA, AAB) with specific handling
|
||||
- Workflow artifacts for any file
|
||||
- Generic artifacts for any file
|
||||
- Upload via `ctx.uploadArtifact()` or `upload-artifact` step
|
||||
|
||||
## Package Interdependencies
|
||||
|
||||
@@ -16,13 +16,11 @@ function createCtx(
|
||||
{ workingdir = '/tmp/wd' }: { workingdir?: string } = {}
|
||||
): {
|
||||
ctx: BuildContext<Ios.Job>;
|
||||
info: jest.Mock;
|
||||
warn: jest.Mock;
|
||||
} {
|
||||
const info = jest.fn();
|
||||
const warn = jest.fn();
|
||||
const logger = {
|
||||
info,
|
||||
info: jest.fn(),
|
||||
debug: jest.fn(),
|
||||
error: jest.fn(),
|
||||
warn,
|
||||
@@ -38,7 +36,7 @@ function createCtx(
|
||||
workingdir,
|
||||
}
|
||||
);
|
||||
return { ctx, info, warn };
|
||||
return { ctx, warn };
|
||||
}
|
||||
|
||||
// Project checkout lives at <workingdir>/build (job projectRootDirectory is '.').
|
||||
@@ -81,31 +79,22 @@ describe(parseJobHooksAsync, () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('writes nothing to the build log while parsing', async () => {
|
||||
const { ctx, info, warn } = createCtx({
|
||||
before_install_node_modules: [{ run: 'echo hi' }],
|
||||
before_submit: [{ run: 'echo hi' }],
|
||||
before_bogus: [{ run: 'echo hi' }],
|
||||
});
|
||||
await parseJobHooksAsync(ctx, INSTALL);
|
||||
expect(info).not.toHaveBeenCalled();
|
||||
expect(warn).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('treats an unknown hook key as inert', async () => {
|
||||
const { ctx } = createCtx({
|
||||
it('treats an unknown hook key as inert and warns', async () => {
|
||||
const { ctx, warn } = createCtx({
|
||||
before_bogus: [{ run: 'echo hi' }],
|
||||
});
|
||||
const parsed = await parseJobHooksAsync(ctx, INSTALL);
|
||||
expect(parsed!.hookEntriesByKey).toEqual({});
|
||||
expect(warn).toHaveBeenCalledWith(expect.stringContaining('unknown hook key "before_bogus"'));
|
||||
});
|
||||
|
||||
it('shape-checks a registered-but-unwrapped key, then skips it', async () => {
|
||||
const { ctx } = createCtx({
|
||||
it('shape-checks a registered-but-unwrapped key, then warns and skips it', async () => {
|
||||
const { ctx, warn } = createCtx({
|
||||
before_submit: [{ run: 'echo hi' }],
|
||||
});
|
||||
const parsed = await parseJobHooksAsync(ctx, INSTALL);
|
||||
expect(parsed!.hookEntriesByKey.before_submit).toBeUndefined();
|
||||
expect(warn).toHaveBeenCalledWith(expect.stringContaining('hooks.before_submit'));
|
||||
});
|
||||
|
||||
it('shape-validates a registered-but-unwrapped key even though it is skipped', async () => {
|
||||
@@ -203,9 +192,10 @@ describe(parseJobHooksAsync, () => {
|
||||
});
|
||||
|
||||
it('never loads a composite function referenced under a registered-but-unwrapped key', async () => {
|
||||
const { ctx } = createCtx({ before_submit: [{ uses: './.eas/functions/missing' }] });
|
||||
const { ctx, warn } = createCtx({ before_submit: [{ uses: './.eas/functions/missing' }] });
|
||||
const parsed = await parseJobHooksAsync(ctx, INSTALL);
|
||||
expect(parsed!.hookEntriesByKey).toEqual({});
|
||||
expect(warn).toHaveBeenCalledWith(expect.stringContaining('hooks.before_submit'));
|
||||
});
|
||||
|
||||
it('wraps a cross-key duplicate step id in the aggregate error', async () => {
|
||||
|
||||
@@ -12,7 +12,6 @@ import spawn from '@expo/turtle-spawn';
|
||||
import { randomBytes, randomUUID } from 'crypto';
|
||||
import { vol } from 'memfs';
|
||||
import fetch, { Response } from 'node-fetch';
|
||||
import { Readable } from 'stream';
|
||||
import { setTimeout } from 'timers/promises';
|
||||
|
||||
import { createMockLogger } from '../../__tests__/utils/logger';
|
||||
@@ -391,13 +390,7 @@ describe('projectSources', () => {
|
||||
ctx.graphqlClient.mutation = mockGraphqlMutation as any;
|
||||
|
||||
const fetchMock = jest.mocked(fetch);
|
||||
let uploadedBody = '';
|
||||
fetchMock.mockImplementation(async (_url, request) => {
|
||||
for await (const chunk of request!.body as Readable) {
|
||||
uploadedBody += chunk;
|
||||
}
|
||||
return { ok: true } as Response;
|
||||
});
|
||||
fetchMock.mockImplementation(async () => ({ ok: true }) as Response);
|
||||
fetchMock.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: async () => ({ data: ctx.job.projectArchive }),
|
||||
@@ -421,14 +414,10 @@ describe('projectSources', () => {
|
||||
'https://storage.example.com/upload',
|
||||
expect.objectContaining({
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'x-custom-header': 'value',
|
||||
'content-length': '63',
|
||||
'content-type': 'text/plain;charset=UTF-8',
|
||||
},
|
||||
headers: { 'x-custom-header': 'value' },
|
||||
body: '{"archiveContent":["project/README.md","project/package.json"]}',
|
||||
})
|
||||
);
|
||||
expect(uploadedBody).toBe('{"archiveContent":["project/README.md","project/package.json"]}');
|
||||
|
||||
// Verify build metadata was updated
|
||||
expect(mockGraphqlMutation).toHaveBeenNthCalledWith(
|
||||
|
||||
@@ -44,8 +44,9 @@ export async function runHookableBuildPhaseAsync<TJob extends BuildJob, T>({
|
||||
if (hooks) {
|
||||
try {
|
||||
await runHookEntriesAsync({ ctx, hooks, timing: 'after', anchor, anchorFailed: true });
|
||||
} catch {
|
||||
// The phase error wins.
|
||||
} catch (hookError) {
|
||||
// The phase error wins; still surface the after-hook failure in the log.
|
||||
ctx.logger.error({ err: hookError }, `after_${anchor} hook failed`);
|
||||
}
|
||||
}
|
||||
throw phaseError;
|
||||
|
||||
@@ -53,17 +53,19 @@ export async function parseJobHooksAsync<TJob extends BuildJob>(
|
||||
const globalContext = new BuildStepGlobalContext(customBuildContext, false);
|
||||
// Hook steps run in the checked-out project directory, not the pre-checkout
|
||||
// target directory the global context defaults to.
|
||||
globalContext.markAsCheckedOut();
|
||||
globalContext.markAsCheckedOut(ctx.logger);
|
||||
|
||||
const externalFunctions = getEasFunctions(customBuildContext);
|
||||
const externalFunctionGroups = getEasFunctionGroups(customBuildContext);
|
||||
|
||||
// Shape-validate every REGISTERED key, reachable or not (steps-world parity:
|
||||
// a malformed `before_submit` fails a steps build, so it fails here too). An
|
||||
// unknown key is inert — a key newer than this worker must be ignored, never
|
||||
// fail the job.
|
||||
// a malformed `before_submit` fails a steps build, so it fails here too), and
|
||||
// warn on keys this build won't run. An unknown key is inert — a key newer
|
||||
// than this worker must be ignored, never fail the job.
|
||||
for (const [key, steps] of Object.entries(hooks)) {
|
||||
if (parseHookKey(key) === null) {
|
||||
const parsed = parseHookKey(key);
|
||||
if (parsed === null) {
|
||||
ctx.logger.warn(`Ignoring unknown hook key "${key}".`);
|
||||
continue;
|
||||
}
|
||||
if (Array.isArray(steps) && steps.length === 0) {
|
||||
@@ -79,6 +81,15 @@ export async function parseJobHooksAsync<TJob extends BuildJob>(
|
||||
{ cause: err }
|
||||
);
|
||||
}
|
||||
// Registered but not wrapped by this build: shape-checked above, then
|
||||
// skipped. Function existence is deliberately NOT checked (that happens only
|
||||
// during construction) — an unknown `uses:` under an unwrapped key warns
|
||||
// rather than failing the build.
|
||||
if (!wrappedAnchors.includes(parsed.anchorId)) {
|
||||
ctx.logger.warn(
|
||||
`Ignoring "hooks.${key}": this build does not run the "${parsed.anchorId}" step.`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Construct entries for the wrapped anchors in EXECUTION order (per anchor,
|
||||
|
||||
@@ -5,13 +5,13 @@ import { asyncResult } from '@expo/results';
|
||||
import spawn from '@expo/turtle-spawn';
|
||||
import fs from 'fs/promises';
|
||||
import { graphql } from 'gql.tada';
|
||||
import fetch from 'node-fetch';
|
||||
import nullthrows from 'nullthrows';
|
||||
import path from 'path';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { shallowCloneRepositoryAsync } from './git';
|
||||
import { BuildContext } from '../context';
|
||||
import { uploadContentWithSignedUrl } from '../storage/uploadWithSignedUrl';
|
||||
import { turtleFetch } from '../utils/turtleFetch';
|
||||
|
||||
export async function prepareProjectSourcesAsync<TJob extends Job>(
|
||||
@@ -208,9 +208,10 @@ async function uploadProjectMetadataAsync(
|
||||
headers: Record<string, string>;
|
||||
};
|
||||
|
||||
await uploadContentWithSignedUrl({
|
||||
signedUrl: uploadSession,
|
||||
content: JSON.stringify({ archiveContent: sortedFiles }),
|
||||
await fetch(uploadSession.url, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify({ archiveContent: sortedFiles }),
|
||||
headers: uploadSession.headers,
|
||||
});
|
||||
|
||||
const updateMetadataResult = await ctx.graphqlClient
|
||||
|
||||
@@ -4,11 +4,13 @@ import {
|
||||
BuildPhaseResult,
|
||||
Env,
|
||||
EnvironmentSecretType,
|
||||
GenericArtifactType,
|
||||
Job,
|
||||
LogMarker,
|
||||
ManagedArtifactType,
|
||||
Metadata,
|
||||
errors,
|
||||
isGenericArtifact,
|
||||
} from '@expo/eas-build-job';
|
||||
import { BuildTrigger } from '@expo/eas-build-job/dist/common';
|
||||
import { bunyan } from '@expo/logger';
|
||||
@@ -40,7 +42,7 @@ export type ArtifactToUpload =
|
||||
paths: string[];
|
||||
}
|
||||
| {
|
||||
type: null;
|
||||
type: GenericArtifactType;
|
||||
name: string;
|
||||
paths: string[];
|
||||
metadata?: Record<string, unknown>;
|
||||
@@ -239,7 +241,7 @@ export class BuildContext<TJob extends Job = Job> {
|
||||
logger: bunyan;
|
||||
}): Promise<{ artifactId: string | null }> {
|
||||
const result = await this._uploadArtifact({ artifact, logger });
|
||||
if (result.filename && artifact.type !== null) {
|
||||
if (result.filename && !isGenericArtifact(artifact)) {
|
||||
this.artifacts[artifact.type] = result.filename;
|
||||
}
|
||||
return { artifactId: result.artifactId ?? null };
|
||||
|
||||
@@ -3,16 +3,8 @@ import HttpLogStream from './logging/HttpLogStream';
|
||||
import RemoteLoggerStream from './logging/RemoteLoggerStream';
|
||||
|
||||
export { Builders, HttpLogStream, RemoteLoggerStream };
|
||||
export {
|
||||
SignedUrlUploadApiError,
|
||||
SignedUrlUploadNetworkError,
|
||||
uploadWithSignedUrl,
|
||||
} from './storage/uploadWithSignedUrl';
|
||||
export type {
|
||||
SignedUrl,
|
||||
UploadWithSignedUrlOptions,
|
||||
UploadWithSignedUrlParams,
|
||||
} from './storage/uploadWithSignedUrl';
|
||||
export { uploadWithSignedUrl } from './storage/uploadWithSignedUrl';
|
||||
export type { SignedUrl, UploadWithSignedUrlParams } from './storage/uploadWithSignedUrl';
|
||||
export { downloadCacheAsync } from './steps/functions/restoreCache';
|
||||
export { uploadCacheAsync } from './steps/functions/saveCache';
|
||||
|
||||
|
||||
@@ -56,10 +56,10 @@ class RemoteLoggerStream extends Writable {
|
||||
return null;
|
||||
}
|
||||
|
||||
public async init(): Promise<void> {
|
||||
public async init(): Promise<string> {
|
||||
this.fileHandle = await fs.open(this.temporaryLogsPath, 'w+', 0o660);
|
||||
this.flushInterval = setInterval(() => this.flush(), this.options.uploadIntervalMs);
|
||||
await this.flush(true);
|
||||
return (await this.flush(true)) as string;
|
||||
}
|
||||
|
||||
public async cleanUp(): Promise<void> {
|
||||
@@ -121,18 +121,17 @@ class RemoteLoggerStream extends Writable {
|
||||
}
|
||||
}
|
||||
|
||||
private async flush(force = false): Promise<void> {
|
||||
private async flush(force = false): Promise<string | void> {
|
||||
if (force || this.hasChangesToUpload) {
|
||||
if (this.uploadingPromise) {
|
||||
await this.uploadingPromise;
|
||||
await this.flush(force);
|
||||
return;
|
||||
return await this.flush(force);
|
||||
}
|
||||
await this.flushInternal();
|
||||
return await this.flushInternal();
|
||||
}
|
||||
}
|
||||
|
||||
private async flushInternal(): Promise<void> {
|
||||
private async flushInternal(): Promise<string | void> {
|
||||
await this.safeWriteToFile();
|
||||
|
||||
let resolveFn: PromiseResolveFn;
|
||||
@@ -140,17 +139,21 @@ class RemoteLoggerStream extends Writable {
|
||||
this.uploadingPromise = new Promise(res => {
|
||||
resolveFn = res;
|
||||
});
|
||||
await this.upload()
|
||||
return await this.upload()
|
||||
.then(result => {
|
||||
return result;
|
||||
})
|
||||
.catch(err => {
|
||||
this.logger.error({ err }, 'Failed to upload logs file');
|
||||
})
|
||||
.then(() => {
|
||||
.then(result => {
|
||||
this.uploadingPromise = undefined;
|
||||
resolveFn();
|
||||
return result;
|
||||
});
|
||||
}
|
||||
|
||||
private async upload(): Promise<void> {
|
||||
private async upload(): Promise<string | void> {
|
||||
if (!this.uploadMethod) {
|
||||
return;
|
||||
}
|
||||
@@ -162,7 +165,7 @@ class RemoteLoggerStream extends Writable {
|
||||
);
|
||||
};
|
||||
|
||||
await uploadWithSignedUrl({
|
||||
return await uploadWithSignedUrl({
|
||||
signedUrl: this.uploadMethod.signedUrl,
|
||||
srcGeneratorAsync,
|
||||
});
|
||||
|
||||
@@ -48,10 +48,7 @@ describe(createCheckoutBuildFunction, () => {
|
||||
});
|
||||
|
||||
it('moves project sources to the target directory when no inputs are given', async () => {
|
||||
const logger = createMockLogger();
|
||||
jest.mocked(logger.child).mockReturnValue(logger);
|
||||
const { globalCtx, executeAsync } = createCheckoutStep({
|
||||
logger,
|
||||
projectArchive: GIT_PROJECT_ARCHIVE,
|
||||
});
|
||||
|
||||
@@ -59,9 +56,6 @@ describe(createCheckoutBuildFunction, () => {
|
||||
|
||||
expect(await vol.promises.readFile(`${projectTargetDirectory}/app.json`, 'utf8')).toBe('{}');
|
||||
expect(globalCtx.wasCheckedOut()).toBe(true);
|
||||
expect(logger.info).toHaveBeenCalledWith(
|
||||
expect.stringContaining('Changing default working directory to')
|
||||
);
|
||||
expect(fetchAndCheckoutRefAsync).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
@@ -102,11 +96,14 @@ describe(createCheckoutBuildFunction, () => {
|
||||
});
|
||||
|
||||
it('rejects the ref input when the project is already checked out', async () => {
|
||||
const logger = createMockLogger();
|
||||
jest.mocked(logger.child).mockReturnValue(logger);
|
||||
const { globalCtx, executeAsync } = createCheckoutStep({
|
||||
logger,
|
||||
projectArchive: GIT_PROJECT_ARCHIVE,
|
||||
callInputs: { ref: 'feature/add-icon' },
|
||||
});
|
||||
globalCtx.markAsCheckedOut();
|
||||
globalCtx.markAsCheckedOut(logger);
|
||||
|
||||
await expect(executeAsync()).rejects.toMatchObject({
|
||||
errorCode: 'EAS_CHECKOUT_REF_AFTER_CHECKOUT',
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { SystemError, UserError } from '@expo/eas-build-job';
|
||||
import { GenericArtifactType, SystemError, UserError } from '@expo/eas-build-job';
|
||||
import spawn from '@expo/turtle-spawn';
|
||||
import fs from 'fs/promises';
|
||||
import os from 'os';
|
||||
@@ -305,6 +305,7 @@ describe('createMaestroTestsBuildFunction', () => {
|
||||
expect(mockUploadArtifact).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
artifact: expect.objectContaining({
|
||||
type: GenericArtifactType.OTHER,
|
||||
name:
|
||||
format === 'html' ? 'Maestro Runner HTML Report' : 'Maestro Runner Allure Results',
|
||||
paths: [
|
||||
@@ -1090,6 +1091,7 @@ describe('createMaestroTestsBuildFunction', () => {
|
||||
expect(mockUploadArtifact).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
artifact: expect.objectContaining({
|
||||
type: GenericArtifactType.OTHER,
|
||||
name: 'Maestro HTML Report',
|
||||
paths: ['/home/expo/.maestro/tests/android-maestro-html.html'],
|
||||
}),
|
||||
@@ -1418,6 +1420,7 @@ describe('createMaestroTestsBuildFunction', () => {
|
||||
expect(mockUploadArtifact).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
artifact: expect.objectContaining({
|
||||
type: GenericArtifactType.OTHER,
|
||||
name: shot.displayName,
|
||||
metadata: shot.metadata,
|
||||
}),
|
||||
|
||||
@@ -121,32 +121,26 @@ describe(createUploadArtifactBuildFunction, () => {
|
||||
expect(typeInput.isRawValueOneOfAllowedValues()).toBe(false);
|
||||
});
|
||||
|
||||
it.each(['other', 'android-apk', 'android-aab', 'ios-simulator-app', 'ios-ipa'])(
|
||||
'uploads %s as a workflow artifact with metadata',
|
||||
async type => {
|
||||
contextUploadArtifact.mockClear();
|
||||
const metadata = { kind: 'screen-recording', startedAtMs: '123', device: 'emulator-5554' };
|
||||
const buildStep = uploadArtifact.createBuildStepFromFunctionCall(
|
||||
createGlobalContextMock({}),
|
||||
{
|
||||
callInputs: {
|
||||
type,
|
||||
key: 'Screen Recording',
|
||||
path: '/',
|
||||
metadata,
|
||||
},
|
||||
}
|
||||
);
|
||||
it('passes the metadata input through to runtimeApi.uploadArtifact', async () => {
|
||||
contextUploadArtifact.mockClear();
|
||||
const metadata = { kind: 'screen-recording', startedAtMs: '123', device: 'emulator-5554' };
|
||||
const buildStep = uploadArtifact.createBuildStepFromFunctionCall(createGlobalContextMock({}), {
|
||||
callInputs: {
|
||||
// Generic ('other') artifact: only generic artifacts persist metadata in the worker
|
||||
// (the managed-type upload paths drop it), so that's the type metadata is meant for.
|
||||
type: 'other',
|
||||
key: 'Screen Recording',
|
||||
path: '/',
|
||||
metadata,
|
||||
},
|
||||
});
|
||||
|
||||
await buildStep.executeAsync();
|
||||
await buildStep.executeAsync();
|
||||
|
||||
expect(contextUploadArtifact).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
artifact: expect.objectContaining({ type: null, metadata }),
|
||||
})
|
||||
);
|
||||
}
|
||||
);
|
||||
expect(contextUploadArtifact).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ artifact: expect.objectContaining({ metadata }) })
|
||||
);
|
||||
});
|
||||
|
||||
it('does not attach metadata to managed artifact types (the worker drops it)', async () => {
|
||||
contextUploadArtifact.mockClear();
|
||||
|
||||
@@ -58,11 +58,7 @@ export function createCheckoutBuildFunction(): BuildFunction {
|
||||
});
|
||||
}
|
||||
|
||||
const previousWorkingDirectory = stepsCtx.global.defaultWorkingDirectory;
|
||||
stepsCtx.global.markAsCheckedOut();
|
||||
stepsCtx.logger.info(
|
||||
`Changing default working directory to ${stepsCtx.global.defaultWorkingDirectory} (was ${previousWorkingDirectory})`
|
||||
);
|
||||
stepsCtx.global.markAsCheckedOut(stepsCtx.logger);
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { SystemError, UserError } from '@expo/eas-build-job';
|
||||
import { GenericArtifactType, SystemError, UserError } from '@expo/eas-build-job';
|
||||
import { bunyan } from '@expo/logger';
|
||||
import {
|
||||
BuildFunction,
|
||||
@@ -631,7 +631,7 @@ export function createMaestroTestsBuildFunction(ctx: CustomBuildContext): BuildF
|
||||
try {
|
||||
await ctx.runtimeApi.uploadArtifact({
|
||||
artifact: {
|
||||
type: null,
|
||||
type: GenericArtifactType.OTHER,
|
||||
name: selectedReport.name,
|
||||
paths: [selectedReport.artifactPath],
|
||||
},
|
||||
@@ -738,7 +738,7 @@ async function uploadFailureScreenshotsAsync({
|
||||
await fs.copyFile(shot.fileAbsPath, safePath);
|
||||
await ctx.runtimeApi.uploadArtifact({
|
||||
artifact: {
|
||||
type: null,
|
||||
type: GenericArtifactType.OTHER,
|
||||
name: shot.displayName,
|
||||
paths: [safePath],
|
||||
metadata: shot.metadata,
|
||||
|
||||
@@ -11,7 +11,6 @@ import path from 'path';
|
||||
import * as tar from 'tar';
|
||||
import z from 'zod';
|
||||
|
||||
import { uploadWithSignedUrl } from '../../storage/uploadWithSignedUrl';
|
||||
import { formatBytes } from '../../utils/artifacts';
|
||||
import { retryOnDNSFailure } from '../../utils/retryOnDNSFailure';
|
||||
import { getCacheVersion } from '../utils/cache';
|
||||
@@ -162,13 +161,16 @@ export async function uploadCacheAsync({
|
||||
|
||||
logger.info(`Uploading cache...`);
|
||||
|
||||
await uploadWithSignedUrl(
|
||||
{
|
||||
signedUrl: { url, headers },
|
||||
srcGeneratorAsync: async () => fs.createReadStream(archivePath),
|
||||
},
|
||||
{ retries: 3, retryIntervalMs: 100 }
|
||||
);
|
||||
const uploadResponse = await retryOnDNSFailure(fetch)(new URL(url), {
|
||||
method: 'PUT',
|
||||
headers,
|
||||
body: fs.createReadStream(archivePath),
|
||||
});
|
||||
if (!uploadResponse.ok) {
|
||||
throw new Error(
|
||||
`Unexpected response from cache server (${uploadResponse.status}): ${uploadResponse.statusText}`
|
||||
);
|
||||
}
|
||||
logger.info(`Uploaded cache archive to ${archivePath} (${formatBytes(size)}).`);
|
||||
}
|
||||
|
||||
@@ -224,13 +226,16 @@ export async function uploadPublicCacheAsync({
|
||||
|
||||
logger.info(`Uploading public cache...`);
|
||||
|
||||
await uploadWithSignedUrl(
|
||||
{
|
||||
signedUrl: { url, headers },
|
||||
srcGeneratorAsync: async () => fs.createReadStream(archivePath),
|
||||
},
|
||||
{ retries: 3, retryIntervalMs: 100 }
|
||||
);
|
||||
const uploadResponse = await retryOnDNSFailure(fetch)(new URL(url), {
|
||||
method: 'PUT',
|
||||
headers,
|
||||
body: fs.createReadStream(archivePath),
|
||||
});
|
||||
if (!uploadResponse.ok) {
|
||||
throw new Error(
|
||||
`Unexpected response from cache server (${uploadResponse.status}): ${uploadResponse.statusText}`
|
||||
);
|
||||
}
|
||||
logger.info(`Uploaded cache archive to ${archivePath} (${formatBytes(size)}).`);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
import { Job, ManagedArtifactType } from '@expo/eas-build-job';
|
||||
import {
|
||||
GenericArtifactType,
|
||||
Job,
|
||||
ManagedArtifactType,
|
||||
isGenericArtifact,
|
||||
} from '@expo/eas-build-job';
|
||||
import {
|
||||
BuildFunction,
|
||||
BuildStepInput,
|
||||
@@ -28,12 +33,7 @@ export function createUploadArtifactBuildFunction(ctx: CustomBuildContext): Buil
|
||||
ManagedArtifactType.APPLICATION_ARCHIVE,
|
||||
ManagedArtifactType.BUILD_ARTIFACTS,
|
||||
...Object.keys(artifactTypeInputToManagedArtifactType),
|
||||
// Keep accepting these legacy artifact types for compatibility.
|
||||
'android-apk',
|
||||
'android-aab',
|
||||
'ios-simulator-app',
|
||||
'ios-ipa',
|
||||
'other',
|
||||
...Object.values(GenericArtifactType),
|
||||
],
|
||||
required: false,
|
||||
allowedValueTypeName: BuildStepInputValueTypeName.STRING,
|
||||
@@ -126,13 +126,12 @@ export function createUploadArtifactBuildFunction(ctx: CustomBuildContext): Buil
|
||||
paths: artifactPaths,
|
||||
name: (inputs.name.value || inputs.key.value) as string,
|
||||
};
|
||||
const artifact =
|
||||
artifactSpec.type === null
|
||||
? {
|
||||
...artifactSpec,
|
||||
metadata: inputs.metadata.value as Record<string, unknown> | undefined,
|
||||
}
|
||||
: artifactSpec;
|
||||
const artifact = isGenericArtifact(artifactSpec)
|
||||
? {
|
||||
...artifactSpec,
|
||||
metadata: inputs.metadata.value as Record<string, unknown> | undefined,
|
||||
}
|
||||
: artifactSpec;
|
||||
|
||||
try {
|
||||
const { artifactId } = await ctx.runtimeApi.uploadArtifact({ artifact, logger });
|
||||
@@ -142,7 +141,7 @@ export function createUploadArtifactBuildFunction(ctx: CustomBuildContext): Buil
|
||||
}
|
||||
} catch (error) {
|
||||
if (inputs.ignore_error.value) {
|
||||
logger.error({ err: error }, 'Failed to upload artifact. Ignoring error.');
|
||||
logger.error({ err: error }, `Failed to upload ${artifact.type}. Ignoring error.`);
|
||||
// Ignoring error.
|
||||
return;
|
||||
}
|
||||
@@ -159,7 +158,7 @@ export function createUploadArtifactBuildFunction(ctx: CustomBuildContext): Buil
|
||||
* values. This makes sure we support all:
|
||||
* - kebab-case managed artifact types (the original)
|
||||
* - snake-caps-case managed artifact types (the mistake)
|
||||
* - workflow artifact types.
|
||||
* - generic artifact types.
|
||||
*/
|
||||
function parseArtifactTypeInput({
|
||||
inputValue,
|
||||
@@ -167,15 +166,19 @@ function parseArtifactTypeInput({
|
||||
}: {
|
||||
inputValue: string;
|
||||
platform: Job['platform'];
|
||||
}): ManagedArtifactType | null {
|
||||
}): GenericArtifactType | ManagedArtifactType {
|
||||
if (!inputValue) {
|
||||
// In build jobs the default artifact type is application-archive.
|
||||
return platform ? ManagedArtifactType.APPLICATION_ARCHIVE : null;
|
||||
return platform ? ManagedArtifactType.APPLICATION_ARCHIVE : GenericArtifactType.OTHER;
|
||||
}
|
||||
|
||||
return (
|
||||
artifactTypeInputToManagedArtifactType[inputValue] ??
|
||||
Object.values(ManagedArtifactType).find(type => type === inputValue) ??
|
||||
null
|
||||
);
|
||||
// Step's allowedValues ensures input is either
|
||||
// a key of artifactTypeInputToManagedArtifactType
|
||||
// or a value of an artifact type.
|
||||
const translatedManagedArtifactType = artifactTypeInputToManagedArtifactType[inputValue];
|
||||
if (translatedManagedArtifactType) {
|
||||
return translatedManagedArtifactType;
|
||||
}
|
||||
|
||||
return inputValue as GenericArtifactType | ManagedArtifactType;
|
||||
}
|
||||
@@ -18,14 +18,7 @@ import {
|
||||
} from '../deviceRunSessionPreview';
|
||||
import { DEVICE_RUN_SESSION_TOKEN_HEADER } from '../deviceRunSessionWorkerApi';
|
||||
|
||||
jest.mock('node-fetch', () => {
|
||||
const actual = jest.requireActual('node-fetch');
|
||||
return {
|
||||
__esModule: true,
|
||||
...actual,
|
||||
default: jest.fn(),
|
||||
};
|
||||
});
|
||||
jest.mock('node-fetch');
|
||||
jest.mock('@expo/turtle-spawn');
|
||||
jest.mock('../../../utils/turtleFetch');
|
||||
const { Response } = jest.requireActual('node-fetch') as typeof import('node-fetch');
|
||||
@@ -94,8 +87,8 @@ describe(startDeviceRunSessionPreview, () => {
|
||||
'https://uploads.expo.test/preview',
|
||||
expect.objectContaining({
|
||||
method: 'PUT',
|
||||
body: expect.any(Readable),
|
||||
headers: { 'content-type': 'image/webp', 'content-length': String(image.length) },
|
||||
body: image,
|
||||
headers: { 'content-type': 'image/webp' },
|
||||
})
|
||||
);
|
||||
await jest.advanceTimersByTimeAsync(59_999);
|
||||
@@ -137,7 +130,9 @@ describe(startDeviceRunSessionPreview, () => {
|
||||
await jest.advanceTimersByTimeAsync(0);
|
||||
expect(logger.warn).toHaveBeenCalledTimes(1);
|
||||
const [fields] = jest.mocked(logger.warn).mock.calls[0] as unknown as [{ err: Error }];
|
||||
expect(fields.err.message).toBe('Signed URL upload failed: network error.');
|
||||
expect(fields.err.message).toBe(
|
||||
'Session preview upload request failed (name: FetchError, type: request-timeout).'
|
||||
);
|
||||
expect(JSON.stringify(jest.mocked(logger.warn).mock.calls)).not.toContain('uploads.expo.test');
|
||||
});
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { SystemError } from '@expo/eas-build-job';
|
||||
import { FetchError } from 'node-fetch';
|
||||
|
||||
import { CustomBuildContext } from '../../../customBuildContext';
|
||||
import { turtleFetch } from '../../../utils/turtleFetch';
|
||||
@@ -12,7 +11,6 @@ import {
|
||||
} from '../deviceRunSessionWorkerApi';
|
||||
|
||||
jest.mock('../../../utils/turtleFetch');
|
||||
jest.unmock('node-fetch');
|
||||
|
||||
const { Response } = jest.requireActual('node-fetch') as typeof import('node-fetch');
|
||||
type Response = InstanceType<typeof Response>;
|
||||
@@ -264,12 +262,12 @@ describe(DeviceRunSessionWorkerApi, () => {
|
||||
it('rejects an application response without the build field', async () => {
|
||||
jest.mocked(turtleFetch).mockResolvedValueOnce(jsonResponse({ data: { platform: 'ios' } }));
|
||||
|
||||
await expect(createApi().getApplicationAsync({ retries: 3 })).rejects.toThrow(
|
||||
await expect(createApi().getApplicationAsync()).rejects.toThrow(
|
||||
'GET /v2/device-run-session-worker/application returned an unexpected response'
|
||||
);
|
||||
});
|
||||
|
||||
it('passes the signal and timeout without enabling nested retries', async () => {
|
||||
it('passes the signal, timeout, and retries to the request', async () => {
|
||||
jest.mocked(turtleFetch).mockResolvedValueOnce(jsonResponse({ data: { iceServers: [] } }));
|
||||
const controller = new AbortController();
|
||||
|
||||
@@ -282,7 +280,7 @@ describe(DeviceRunSessionWorkerApi, () => {
|
||||
expect(jest.mocked(turtleFetch)).toHaveBeenCalledWith(
|
||||
expect.any(String),
|
||||
'POST',
|
||||
expect.objectContaining({ signal: controller.signal, timeout: 5000, retries: 0 })
|
||||
expect.objectContaining({ signal: controller.signal, timeout: 5000, retries: 1 })
|
||||
);
|
||||
});
|
||||
|
||||
@@ -321,7 +319,7 @@ describe(DeviceRunSessionWorkerApi, () => {
|
||||
);
|
||||
|
||||
const error = await createApi()
|
||||
.getSessionAsync({ retries: 0 })
|
||||
.getSessionAsync()
|
||||
.catch(err => err);
|
||||
|
||||
expect(error).toBeInstanceOf(DeviceRunSessionWorkerApiError);
|
||||
@@ -336,7 +334,7 @@ describe(DeviceRunSessionWorkerApi, () => {
|
||||
jest.mocked(turtleFetch).mockRejectedValueOnce(reason);
|
||||
|
||||
const error = await createApi()
|
||||
.getSessionAsync({ retries: 0 })
|
||||
.getSessionAsync()
|
||||
.catch(err => err);
|
||||
|
||||
expect(error).toBeInstanceOf(DeviceRunSessionWorkerApiError);
|
||||
@@ -350,7 +348,7 @@ describe(DeviceRunSessionWorkerApi, () => {
|
||||
.mocked(turtleFetch)
|
||||
.mockResolvedValueOnce(jsonResponse({ data: { id: 'drs-id', status: 'STOPPED' } }));
|
||||
|
||||
await expect(createApi().getSessionAsync({ retries: 3 })).rejects.toThrow(
|
||||
await expect(createApi().getSessionAsync()).rejects.toThrow(
|
||||
'GET /v2/device-run-session-worker/session returned an unexpected response'
|
||||
);
|
||||
});
|
||||
@@ -358,183 +356,8 @@ describe(DeviceRunSessionWorkerApi, () => {
|
||||
it('rejects a successful response that is not JSON', async () => {
|
||||
jest.mocked(turtleFetch).mockResolvedValueOnce(new Response('ok', { status: 200 }));
|
||||
|
||||
await expect(createApi().getSessionAsync({ retries: 3 })).rejects.toThrow(
|
||||
await expect(createApi().getSessionAsync()).rejects.toThrow(
|
||||
'GET /v2/device-run-session-worker/session returned a response that is not JSON.'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('device run session worker API retries', () => {
|
||||
beforeEach(() => {
|
||||
jest.useFakeTimers();
|
||||
jest.mocked(turtleFetch).mockReset();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
it.each([429, 500, 502, 503, 504])(
|
||||
'recovers from HTTP %s when starting a session',
|
||||
async status => {
|
||||
jest
|
||||
.mocked(turtleFetch)
|
||||
.mockResolvedValueOnce(new Response('unavailable', { status }))
|
||||
.mockResolvedValueOnce(jsonResponse({ data: { id: 'drs-id', status: 'in-progress' } }));
|
||||
const result = createApi().startAsync(
|
||||
{ remoteConfig: { url: 'https://preview.test' } },
|
||||
{ retries: 3, timeoutMs: 30_000 }
|
||||
);
|
||||
const assertion = expect(result).resolves.toEqual({ id: 'drs-id', status: 'in-progress' });
|
||||
await jest.runAllTimersAsync();
|
||||
await assertion;
|
||||
expect(turtleFetch).toHaveBeenCalledTimes(2);
|
||||
expect(jest.mocked(turtleFetch).mock.calls[1]).toEqual(
|
||||
jest.mocked(turtleFetch).mock.calls[0]
|
||||
);
|
||||
expect(jest.mocked(turtleFetch).mock.calls[0][2]).toMatchObject({
|
||||
retries: 0,
|
||||
timeout: 30_000,
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
it('recovers from a network failure', async () => {
|
||||
jest
|
||||
.mocked(turtleFetch)
|
||||
.mockRejectedValueOnce(new Error('socket hang up'))
|
||||
.mockResolvedValueOnce(jsonResponse({ data: { build: null } }));
|
||||
const assertion = expect(createApi().getApplicationAsync({ retries: 3 })).resolves.toEqual({
|
||||
build: null,
|
||||
});
|
||||
await jest.runAllTimersAsync();
|
||||
await assertion;
|
||||
expect(turtleFetch).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it('exhausts three retries with exponential backoff', async () => {
|
||||
jest
|
||||
.mocked(turtleFetch)
|
||||
.mockImplementation(async () => new Response('unavailable', { status: 503 }));
|
||||
const assertion = expect(
|
||||
createApi().startAsync({ remoteConfig: {} }, { retries: 3 })
|
||||
).rejects.toMatchObject({
|
||||
status: 503,
|
||||
});
|
||||
await jest.advanceTimersByTimeAsync(0);
|
||||
expect(turtleFetch).toHaveBeenCalledTimes(1);
|
||||
await jest.advanceTimersByTimeAsync(1000);
|
||||
expect(turtleFetch).toHaveBeenCalledTimes(2);
|
||||
await jest.advanceTimersByTimeAsync(2000);
|
||||
expect(turtleFetch).toHaveBeenCalledTimes(3);
|
||||
await jest.advanceTimersByTimeAsync(4000);
|
||||
await assertion;
|
||||
expect(turtleFetch).toHaveBeenCalledTimes(4);
|
||||
});
|
||||
|
||||
it.each([400, 401, 403, 404, 409])('does not retry HTTP %s', async status => {
|
||||
jest.mocked(turtleFetch).mockResolvedValueOnce(new Response('refused', { status }));
|
||||
await expect(
|
||||
createApi().startAsync({ remoteConfig: {} }, { retries: 3 })
|
||||
).rejects.toMatchObject({ status });
|
||||
expect(turtleFetch).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it.each([
|
||||
['status polling', (api: DeviceRunSessionWorkerApi) => api.getSessionAsync()],
|
||||
['stop', (api: DeviceRunSessionWorkerApi) => api.stopAsync()],
|
||||
[
|
||||
'screenshot application metadata',
|
||||
(api: DeviceRunSessionWorkerApi) => api.getApplicationAsync(),
|
||||
],
|
||||
['event log setup', (api: DeviceRunSessionWorkerApi) => api.createEventLogUploadSessionAsync()],
|
||||
['preview setup', (api: DeviceRunSessionWorkerApi) => api.createPreviewUploadSessionAsync()],
|
||||
[
|
||||
'TURN without an explicit retry option',
|
||||
(api: DeviceRunSessionWorkerApi) => api.fetchTurnIceServersAsync(),
|
||||
],
|
||||
])('does not retry %s by default', async (_name, call) => {
|
||||
jest.mocked(turtleFetch).mockResolvedValueOnce(new Response('unavailable', { status: 503 }));
|
||||
await expect(call(createApi())).rejects.toMatchObject({ status: 503 });
|
||||
expect(turtleFetch).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('honors an explicit retry count', async () => {
|
||||
jest
|
||||
.mocked(turtleFetch)
|
||||
.mockImplementation(async () => new Response('unavailable', { status: 503 }));
|
||||
const assertion = expect(
|
||||
createApi().fetchTurnIceServersAsync({ retries: 1 })
|
||||
).rejects.toMatchObject({ status: 503 });
|
||||
await jest.runAllTimersAsync();
|
||||
await assertion;
|
||||
expect(turtleFetch).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it('does not send another request after cancellation during backoff', async () => {
|
||||
const controller = new AbortController();
|
||||
jest.mocked(turtleFetch).mockResolvedValueOnce(new Response('unavailable', { status: 503 }));
|
||||
const assertion = expect(
|
||||
createApi().getApplicationAsync({ signal: controller.signal, retries: 3 })
|
||||
).rejects.toMatchObject({ name: 'AbortError' });
|
||||
await jest.advanceTimersByTimeAsync(0);
|
||||
controller.abort();
|
||||
await jest.runAllTimersAsync();
|
||||
await assertion;
|
||||
expect(turtleFetch).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('device run session retry safety', () => {
|
||||
beforeEach(() => {
|
||||
jest.mocked(turtleFetch).mockReset();
|
||||
});
|
||||
|
||||
it('does not replay artifact creation after a transient failure', async () => {
|
||||
jest.mocked(turtleFetch).mockResolvedValueOnce(new Response('unavailable', { status: 503 }));
|
||||
await expect(
|
||||
createApi().createArtifactUploadSessionAsync({
|
||||
name: 'report',
|
||||
filename: 'report.json',
|
||||
size: 1,
|
||||
})
|
||||
).rejects.toMatchObject({ status: 503 });
|
||||
expect(turtleFetch).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('does not send an already cancelled request', async () => {
|
||||
const controller = new AbortController();
|
||||
controller.abort();
|
||||
await expect(
|
||||
createApi().getApplicationAsync({ signal: controller.signal, retries: 3 })
|
||||
).rejects.toMatchObject({
|
||||
name: 'AbortError',
|
||||
});
|
||||
expect(turtleFetch).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it.each(['system', 'body-timeout'])(
|
||||
'retries a response body transport failure (%s)',
|
||||
async type => {
|
||||
jest.useFakeTimers();
|
||||
try {
|
||||
const response = jsonResponse({});
|
||||
jest
|
||||
.spyOn(response, 'json')
|
||||
.mockRejectedValueOnce(new FetchError('connection interrupted', type));
|
||||
jest
|
||||
.mocked(turtleFetch)
|
||||
.mockResolvedValueOnce(response)
|
||||
.mockResolvedValueOnce(jsonResponse({ data: { id: 'drs-id', status: 'in-progress' } }));
|
||||
const assertion = expect(
|
||||
createApi().startAsync({ remoteConfig: {} }, { retries: 3 })
|
||||
).resolves.toMatchObject({ status: 'in-progress' });
|
||||
await jest.runAllTimersAsync();
|
||||
await assertion;
|
||||
expect(turtleFetch).toHaveBeenCalledTimes(2);
|
||||
} finally {
|
||||
jest.useRealTimers();
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
+2
-4
@@ -151,11 +151,10 @@ describe('with a device run session token', () => {
|
||||
expect(ctx.graphqlClient.mutation).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('retries a transient failure when starting the session through the worker API', async () => {
|
||||
it('starts the session through the worker API', async () => {
|
||||
const ctx = createCtx({ withToken: true });
|
||||
jest
|
||||
.mocked(turtleFetch)
|
||||
.mockResolvedValueOnce(jsonResponse({ errors: [{ message: 'Service unavailable' }] }, 503))
|
||||
.mockResolvedValueOnce(jsonResponse({ data: { id: 'drs-id', status: 'in-progress' } }));
|
||||
|
||||
await uploadRemoteSessionConfigAsync({
|
||||
@@ -166,7 +165,6 @@ describe('with a device run session token', () => {
|
||||
});
|
||||
|
||||
expectWorkerApiCall('start', 'POST', { json: { remoteConfig: { type: 'agent-device' } } });
|
||||
expect(turtleFetch).toHaveBeenCalledTimes(2);
|
||||
expect(ctx.graphqlClient.mutation).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
@@ -215,7 +213,7 @@ describe('with a device run session token', () => {
|
||||
'--turn-credential',
|
||||
'credential',
|
||||
]);
|
||||
expectWorkerApiCall('turn-ice-servers', 'POST', { timeout: 5000, retries: 0 });
|
||||
expectWorkerApiCall('turn-ice-servers', 'POST', { timeout: 5000, retries: 1 });
|
||||
expect(jest.mocked(turtleFetch)).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
|
||||
@@ -2,14 +2,6 @@ jest.unmock('fs');
|
||||
jest.unmock('fs/promises');
|
||||
jest.unmock('node:fs');
|
||||
jest.unmock('node:fs/promises');
|
||||
jest.mock('node-fetch', () => {
|
||||
const actual = jest.requireActual('node-fetch');
|
||||
return {
|
||||
__esModule: true,
|
||||
...actual,
|
||||
default: jest.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
import { SandboxDaemonErrorCode, SystemError } from '@expo/eas-build-job';
|
||||
import { type bunyan } from '@expo/logger';
|
||||
@@ -318,7 +310,7 @@ describe('sandbox artifact uploads', () => {
|
||||
expect(logger.error).toHaveBeenCalledTimes(1);
|
||||
const logged = inspect([logger.info.mock.calls, logger.error.mock.calls], { depth: 10 });
|
||||
expect(fetch).toHaveBeenCalledTimes(3);
|
||||
expect(logged).toContain('Signed URL upload failed: ECONNRESET.');
|
||||
expect(logged).toContain('Artifact PUT failed: ECONNRESET.');
|
||||
expect(logged).not.toContain('secret-signature');
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
import { SystemError } from '@expo/eas-build-job';
|
||||
import fetch, { Headers } from 'node-fetch';
|
||||
import type { Readable } from 'node:stream';
|
||||
import { finished } from 'node:stream/promises';
|
||||
import { setTimeout as delay } from 'node:timers/promises';
|
||||
|
||||
import { AbortUtils } from '../../utils/AbortUtils';
|
||||
|
||||
export type ArtifactUploadSession = { url: string; headers: unknown };
|
||||
export type ArtifactSource = { openStream: () => Readable };
|
||||
|
||||
export async function putArtifactWithRetriesAsync(
|
||||
{
|
||||
uploadSession,
|
||||
source,
|
||||
signal,
|
||||
}: {
|
||||
uploadSession: ArtifactUploadSession;
|
||||
source: ArtifactSource;
|
||||
signal?: AbortSignal;
|
||||
},
|
||||
{ attempts = 1 }: { attempts?: number } = {}
|
||||
): Promise<void> {
|
||||
for (let attempt = 0; attempt < attempts; attempt++) {
|
||||
signal?.throwIfAborted();
|
||||
const stream = source.openStream();
|
||||
// Upload time scales with file size, so the deadline bounds stalls, not the whole request.
|
||||
const timeout = AbortUtils.resettableTimeout(90_000);
|
||||
try {
|
||||
await putArtifactAsync({
|
||||
uploadSession,
|
||||
stream,
|
||||
signal: signal ? AbortSignal.any([signal, timeout.signal]) : timeout.signal,
|
||||
onProgress: timeout.resetDeadline,
|
||||
});
|
||||
return;
|
||||
} catch (error) {
|
||||
signal?.throwIfAborted();
|
||||
const retryable =
|
||||
error instanceof ArtifactPutError
|
||||
? [408, 429, 500, 502, 503, 504].includes(error.status)
|
||||
: timeout.signal.aborted || error instanceof ArtifactPutNetworkError;
|
||||
if (!retryable || attempt + 1 === attempts) {
|
||||
throw error;
|
||||
}
|
||||
await delay(500 * (attempt + 1), undefined, { signal });
|
||||
} finally {
|
||||
stream.destroy();
|
||||
await finished(stream).catch(() => {});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function putArtifactAsync({
|
||||
uploadSession,
|
||||
stream,
|
||||
signal,
|
||||
onProgress,
|
||||
}: {
|
||||
uploadSession: ArtifactUploadSession;
|
||||
stream: Readable;
|
||||
signal: AbortSignal;
|
||||
onProgress: () => void;
|
||||
}): Promise<void> {
|
||||
const requestController = new AbortController();
|
||||
try {
|
||||
const responding = fetch(uploadSession.url, {
|
||||
method: 'PUT',
|
||||
headers: new Headers(uploadSession.headers as Record<string, string>),
|
||||
body: stream,
|
||||
signal: AbortSignal.any([signal, requestController.signal]),
|
||||
});
|
||||
// fetch has piped the body already, so this listener observes flow without starting it.
|
||||
stream.on('data', onProgress);
|
||||
let response;
|
||||
try {
|
||||
response = await responding;
|
||||
} catch (error) {
|
||||
signal.throwIfAborted();
|
||||
throw error instanceof Error && error.name === 'FetchError'
|
||||
? new ArtifactPutNetworkError(error)
|
||||
: error;
|
||||
}
|
||||
signal.throwIfAborted();
|
||||
if (!response.ok) {
|
||||
throw new ArtifactPutError(response);
|
||||
}
|
||||
} finally {
|
||||
// node-fetch destroys the request body on abort, and its response stream is a PassThrough
|
||||
// whose destruction leaves the socket open, so the request itself is aborted here.
|
||||
requestController.abort();
|
||||
}
|
||||
}
|
||||
|
||||
export class ArtifactPutError extends SystemError {
|
||||
readonly status: number;
|
||||
readonly reason: string;
|
||||
|
||||
constructor(response: { status: number; statusText: string }) {
|
||||
const reason = `HTTP ${response.status} ${response.statusText}`;
|
||||
super(`Artifact PUT failed: ${reason}.`);
|
||||
this.status = response.status;
|
||||
this.reason = reason;
|
||||
}
|
||||
}
|
||||
|
||||
// node-fetch names the request URL in its message, and here that URL is signed. The message keeps
|
||||
// only the error code, and the original is not kept as the cause because loggers and Sentry print causes.
|
||||
export class ArtifactPutNetworkError extends SystemError {
|
||||
readonly code: string | undefined;
|
||||
readonly reason: string;
|
||||
|
||||
constructor(error: Error & { code?: unknown }) {
|
||||
const code = typeof error.code === 'string' ? error.code : undefined;
|
||||
const reason = code ?? 'network error';
|
||||
super(`Artifact PUT failed: ${reason}.`);
|
||||
this.code = code;
|
||||
this.reason = reason;
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,13 @@
|
||||
import { SystemError } from '@expo/eas-build-job';
|
||||
import type { Readable } from 'node:stream';
|
||||
|
||||
import { CustomBuildContext } from '../../customBuildContext';
|
||||
import {
|
||||
type SignedUrl,
|
||||
SignedUrlUploadApiError,
|
||||
SignedUrlUploadNetworkError,
|
||||
uploadWithSignedUrl,
|
||||
} from '../../storage/uploadWithSignedUrl';
|
||||
ArtifactPutError,
|
||||
ArtifactPutNetworkError,
|
||||
type ArtifactSource,
|
||||
type ArtifactUploadSession,
|
||||
putArtifactWithRetriesAsync,
|
||||
} from './artifactPut';
|
||||
import { withDeviceRunSessionTimeoutAsync } from './deviceRunSessionTimeout';
|
||||
import { requireDeviceRunSessionWorkerApi } from './deviceRunSessionWorkerApi';
|
||||
|
||||
@@ -23,11 +23,7 @@ type ArtifactMetadata = {
|
||||
|
||||
export async function uploadDeviceRunSessionArtifactAsync(
|
||||
ctx: CustomBuildContext,
|
||||
options: ArtifactMetadata & {
|
||||
openStream: () => Readable;
|
||||
signal?: AbortSignal;
|
||||
attempts?: number;
|
||||
}
|
||||
options: ArtifactMetadata & ArtifactSource & { signal?: AbortSignal; attempts?: number }
|
||||
): Promise<void> {
|
||||
try {
|
||||
const uploadSession = await withDeviceRunSessionTimeoutAsync(
|
||||
@@ -35,12 +31,12 @@ export async function uploadDeviceRunSessionArtifactAsync(
|
||||
async signal => await createDeviceRunSessionArtifactUploadSessionAsync(ctx, options, signal)
|
||||
);
|
||||
options.signal?.throwIfAborted();
|
||||
await uploadWithSignedUrl(
|
||||
{ signedUrl: uploadSession, srcGeneratorAsync: async () => options.openStream() },
|
||||
{ signal: options.signal, retries: (options.attempts ?? 1) - 1, retryIntervalMs: 500 }
|
||||
await putArtifactWithRetriesAsync(
|
||||
{ uploadSession, source: options, signal: options.signal },
|
||||
{ attempts: options.attempts }
|
||||
);
|
||||
} catch (error) {
|
||||
if (error instanceof SignedUrlUploadApiError || error instanceof SignedUrlUploadNetworkError) {
|
||||
if (error instanceof ArtifactPutError || error instanceof ArtifactPutNetworkError) {
|
||||
throw new DeviceRunSessionArtifactUploadError(options.artifactId, error.reason);
|
||||
}
|
||||
throw error;
|
||||
@@ -57,9 +53,9 @@ async function createDeviceRunSessionArtifactUploadSessionAsync(
|
||||
ctx: CustomBuildContext,
|
||||
{ deviceRunSessionId, artifactId, name, filename, kind, metadata, size }: ArtifactMetadata,
|
||||
signal: AbortSignal
|
||||
): Promise<SignedUrl> {
|
||||
): Promise<ArtifactUploadSession> {
|
||||
const workerApi = requireDeviceRunSessionWorkerApi(ctx);
|
||||
let uploadSession: SignedUrl;
|
||||
let uploadSession: ArtifactUploadSession;
|
||||
try {
|
||||
uploadSession = await workerApi.createArtifactUploadSessionAsync(
|
||||
{
|
||||
|
||||
@@ -2,6 +2,7 @@ import { type bunyan } from '@expo/logger';
|
||||
import { asyncResult } from '@expo/results';
|
||||
import { BuildRuntimePlatform, type BuildStepEnv } from '@expo/steps';
|
||||
import spawn, { type SpawnOptions } from '@expo/turtle-spawn';
|
||||
import fetch from 'node-fetch';
|
||||
import { createWriteStream } from 'node:fs';
|
||||
import { pipeline } from 'node:stream/promises';
|
||||
import { mkdtemp, readFile, rm } from 'node:fs/promises';
|
||||
@@ -9,7 +10,6 @@ import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
|
||||
import { type CustomBuildContext } from '../../customBuildContext';
|
||||
import { uploadContentWithSignedUrl } from '../../storage/uploadWithSignedUrl';
|
||||
import { killProcessGroup } from '../../utils/processes';
|
||||
import { requireDeviceRunSessionWorkerApi } from './deviceRunSessionWorkerApi';
|
||||
|
||||
@@ -17,6 +17,31 @@ const PREVIEW_INTERVAL_MS = 60_000;
|
||||
const PREVIEW_MAX_DIMENSION = 320;
|
||||
const PREVIEW_QUALITY = 70;
|
||||
const MAX_PREVIEW_SIZE_BYTES = 5 * 1024 * 1024;
|
||||
/**
|
||||
* node-fetch includes the request URL in its error messages, and this URL is a signed upload URL
|
||||
* that is valid for the whole session. Replace such errors with one that names only the failure.
|
||||
*/
|
||||
async function putPreviewAsync(
|
||||
uploadSession: { url: string; headers: Record<string, string> },
|
||||
{ image, signal }: { image: Buffer; signal: AbortSignal }
|
||||
): Promise<Awaited<ReturnType<typeof fetch>>> {
|
||||
try {
|
||||
return await fetch(uploadSession.url, {
|
||||
method: 'PUT',
|
||||
headers: uploadSession.headers,
|
||||
body: image,
|
||||
signal,
|
||||
timeout: 30_000,
|
||||
});
|
||||
} catch (err) {
|
||||
const { name, type, code } = err as { name?: unknown; type?: unknown; code?: unknown };
|
||||
const details = Object.entries({ name, type, code })
|
||||
.filter(([, value]) => typeof value === 'string' && value.length > 0)
|
||||
.map(([key, value]) => `${key}: ${value}`)
|
||||
.join(', ');
|
||||
throw new Error(`Session preview upload request failed (${details || 'unknown error'}).`);
|
||||
}
|
||||
}
|
||||
|
||||
export function startDeviceRunSessionPreview({
|
||||
ctx,
|
||||
@@ -50,10 +75,10 @@ export function startDeviceRunSessionPreview({
|
||||
if (controller.signal.aborted) {
|
||||
return;
|
||||
}
|
||||
await uploadContentWithSignedUrl(
|
||||
{ signedUrl: uploadSession, content: image },
|
||||
{ signal, retries: 0, stallTimeoutMs: 30_000 }
|
||||
);
|
||||
const response = await putPreviewAsync(uploadSession, { image, signal });
|
||||
if (!response.ok) {
|
||||
throw new Error(`Session preview upload failed: ${response.status} ${response.statusText}`);
|
||||
}
|
||||
} catch (err) {
|
||||
if (!controller.signal.aborted) {
|
||||
logger.warn({ err }, 'Could not refresh the session preview; retrying in 60 seconds.');
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
import { type EnvironmentSecret, EnvironmentSecretType, SystemError } from '@expo/eas-build-job';
|
||||
import { type bunyan } from '@expo/logger';
|
||||
import { asyncResult } from '@expo/results';
|
||||
import { FetchError } from 'node-fetch';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { type CustomBuildContext } from '../../customBuildContext';
|
||||
import { promiseRetryWithCondition } from '../../utils/promiseRetryWithCondition';
|
||||
import { turtleFetch } from '../../utils/turtleFetch';
|
||||
|
||||
/**
|
||||
@@ -77,7 +75,7 @@ type RequestOptions = {
|
||||
signal?: AbortSignal;
|
||||
/** node-fetch timeout of each attempt, in milliseconds. */
|
||||
timeoutMs?: number;
|
||||
/** Additional attempts for transient failures. Defaults to none; critical callers opt in. */
|
||||
/** Additional attempts after a failed one. Defaults to none, so each call sends one request. */
|
||||
retries?: number;
|
||||
logger?: bunyan;
|
||||
};
|
||||
@@ -191,44 +189,16 @@ export class DeviceRunSessionWorkerApi {
|
||||
}
|
||||
|
||||
private async requestAsync<T>(
|
||||
method: 'GET' | 'POST',
|
||||
route: string,
|
||||
dataSchema: z.ZodType<T>,
|
||||
{ retries = 0, ...options }: RequestOptions & { json?: Record<string, unknown> }
|
||||
): Promise<T> {
|
||||
const request = () => {
|
||||
options.signal?.throwIfAborted();
|
||||
return this.requestOnceAsync(method, route, dataSchema, options);
|
||||
};
|
||||
if (retries === 0) {
|
||||
return await request();
|
||||
}
|
||||
return await promiseRetryWithCondition(
|
||||
request,
|
||||
error =>
|
||||
!options.signal?.aborted &&
|
||||
error instanceof DeviceRunSessionWorkerApiError &&
|
||||
(error.status === undefined || error.status === 429 || error.status >= 500),
|
||||
{ retries, factor: 2, minTimeout: 1000 },
|
||||
({ attemptNumber, maxAttemptsCount }) =>
|
||||
options.logger?.warn(
|
||||
`Retrying ${method} /v2/${DEVICE_RUN_SESSION_WORKER_PATH}${route} (attempt ${attemptNumber}/${maxAttemptsCount}).`
|
||||
)
|
||||
)();
|
||||
}
|
||||
|
||||
private async requestOnceAsync<T>(
|
||||
method: 'GET' | 'POST',
|
||||
route: string,
|
||||
dataSchema: z.ZodType<T>,
|
||||
{
|
||||
signal,
|
||||
timeoutMs,
|
||||
retries = 0,
|
||||
logger,
|
||||
json,
|
||||
}: Omit<RequestOptions, 'retries'> & {
|
||||
json?: Record<string, unknown>;
|
||||
}
|
||||
}: RequestOptions & { json?: Record<string, unknown> }
|
||||
): Promise<T> {
|
||||
const routeDescription = `${method} /v2/${DEVICE_RUN_SESSION_WORKER_PATH}${route}`;
|
||||
const url = new URL(
|
||||
@@ -242,7 +212,7 @@ export class DeviceRunSessionWorkerApi {
|
||||
...(json ? { json } : {}),
|
||||
...(signal ? { signal } : {}),
|
||||
...(timeoutMs !== undefined ? { timeout: timeoutMs } : {}),
|
||||
retries: 0,
|
||||
retries,
|
||||
shouldThrowOnNotOk: false,
|
||||
logger,
|
||||
})
|
||||
@@ -271,15 +241,6 @@ export class DeviceRunSessionWorkerApi {
|
||||
);
|
||||
}
|
||||
if (!bodyResult.ok) {
|
||||
if (
|
||||
bodyResult.reason instanceof FetchError &&
|
||||
['system', 'body-timeout'].includes(bodyResult.reason.type)
|
||||
) {
|
||||
throw new DeviceRunSessionWorkerApiError(
|
||||
`${routeDescription} failed while reading the response body.`,
|
||||
undefined
|
||||
);
|
||||
}
|
||||
throw new DeviceRunSessionWorkerApiError(
|
||||
`${routeDescription} returned a response that is not JSON.`,
|
||||
response.status
|
||||
|
||||
@@ -560,10 +560,7 @@ export async function uploadRemoteSessionConfigAsync({
|
||||
);
|
||||
const workerApi = requireDeviceRunSessionWorkerApi(ctx);
|
||||
try {
|
||||
await workerApi.startAsync(
|
||||
{ remoteConfig },
|
||||
{ retries: 3, timeoutMs: DEVICE_RUN_SESSION_WORKER_REQUEST_TIMEOUT_MS, logger }
|
||||
);
|
||||
await workerApi.startAsync({ remoteConfig });
|
||||
} catch (err) {
|
||||
throw new SystemError(
|
||||
`Failed to start device run session ${deviceRunSessionId}: ${
|
||||
|
||||
@@ -14,8 +14,12 @@ import { performance } from 'node:perf_hooks';
|
||||
import { Readable } from 'node:stream';
|
||||
import { setTimeout as delay } from 'node:timers/promises';
|
||||
|
||||
import { SignedUrlUploadApiError, uploadWithSignedUrl } from '../../storage/uploadWithSignedUrl';
|
||||
import { graphqlAbortContext } from '../../utils/graphqlAbort';
|
||||
import {
|
||||
ArtifactPutError,
|
||||
type ArtifactUploadSession,
|
||||
putArtifactWithRetriesAsync,
|
||||
} from './artifactPut';
|
||||
|
||||
const CREATE_SANDBOX_ARTIFACT_UPLOAD_SESSION_MUTATION = graphql(`
|
||||
mutation CreateSandboxArtifactUploadSession(
|
||||
@@ -234,23 +238,18 @@ async function putSandboxArtifactAsync({
|
||||
}: {
|
||||
filePath: string;
|
||||
size: number;
|
||||
uploadSession: { url: string; headers: unknown };
|
||||
uploadSession: ArtifactUploadSession;
|
||||
signal: AbortSignal;
|
||||
}): Promise<void> {
|
||||
const openStream = (): Readable =>
|
||||
size === 0 ? Readable.from([]) : createReadStream(filePath, { start: 0, end: size - 1 });
|
||||
try {
|
||||
await uploadWithSignedUrl(
|
||||
{
|
||||
signedUrl: {
|
||||
url: uploadSession.url,
|
||||
headers: uploadSession.headers as Record<string, string>,
|
||||
},
|
||||
srcGeneratorAsync: async () =>
|
||||
size === 0 ? Readable.from([]) : createReadStream(filePath, { start: 0, end: size - 1 }),
|
||||
},
|
||||
{ signal, retries: 2, retryIntervalMs: 500 }
|
||||
await putArtifactWithRetriesAsync(
|
||||
{ uploadSession, source: { openStream }, signal },
|
||||
{ attempts: 3 }
|
||||
);
|
||||
} catch (error) {
|
||||
if (!(error instanceof SignedUrlUploadApiError && error.status === 412)) {
|
||||
if (!(error instanceof ArtifactPutError && error.status === 412)) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,254 +1,325 @@
|
||||
import { type IncomingMessage, type ServerResponse, createServer } from 'node:http';
|
||||
import { performance } from 'node:perf_hooks';
|
||||
import { Readable } from 'node:stream';
|
||||
import { randomBytes } from 'crypto';
|
||||
import fetch, { RequestInit, Response } from 'node-fetch';
|
||||
import { Readable } from 'stream';
|
||||
|
||||
import {
|
||||
SignedUrlUploadApiError,
|
||||
SignedUrlUploadNetworkError,
|
||||
uploadContentWithSignedUrl,
|
||||
uploadWithSignedUrl,
|
||||
} from '../uploadWithSignedUrl';
|
||||
import { type SignedUrl, uploadWithSignedUrl } from '../uploadWithSignedUrl';
|
||||
|
||||
jest.unmock('node-fetch');
|
||||
jest.mock('node-fetch');
|
||||
|
||||
let handle: (request: IncomingMessage, response: ServerResponse) => void | Promise<void>;
|
||||
let server: ReturnType<typeof createServer>;
|
||||
let origin: string;
|
||||
let signedUrl: { url: string; headers: Record<string, string> };
|
||||
let requests: number;
|
||||
class ErrorWithCode extends Error {
|
||||
protected readonly _code: string | undefined;
|
||||
|
||||
async function readBodyAsync(request: IncomingMessage): Promise<string> {
|
||||
const chunks: Buffer[] = [];
|
||||
for await (const chunk of request) {
|
||||
chunks.push(chunk);
|
||||
constructor(message: string, code?: string) {
|
||||
super(message);
|
||||
if (code) {
|
||||
this._code = code;
|
||||
}
|
||||
}
|
||||
|
||||
public get code(): string | undefined {
|
||||
return this._code;
|
||||
}
|
||||
return Buffer.concat(chunks).toString();
|
||||
}
|
||||
|
||||
function respondWithStatuses(statuses: number[]): void {
|
||||
handle = async (request, response) => {
|
||||
await readBodyAsync(request);
|
||||
response.statusCode = statuses[requests - 1];
|
||||
response.end();
|
||||
class DNSError extends ErrorWithCode {
|
||||
protected readonly _code: 'ENOTFOUND' | 'EAI_AGAIN' = 'ENOTFOUND';
|
||||
}
|
||||
|
||||
const TEST_BUCKET = 'turtle-v2-test';
|
||||
|
||||
function createSignedUrl(key: string, contentType: string): SignedUrl {
|
||||
return {
|
||||
url: `https://storage.googleapis.com/${TEST_BUCKET}/${key}?signature=test`,
|
||||
headers: { 'content-type': contentType },
|
||||
};
|
||||
}
|
||||
|
||||
beforeEach(async () => {
|
||||
requests = 0;
|
||||
server = createServer((request, response) => {
|
||||
requests++;
|
||||
void handle(request, response);
|
||||
});
|
||||
await new Promise<void>(resolve => server.listen(0, '127.0.0.1', resolve));
|
||||
const address = server.address();
|
||||
if (!address || typeof address === 'string') {
|
||||
throw new Error('Missing test server address');
|
||||
}
|
||||
origin = `http://127.0.0.1:${address.port}`;
|
||||
signedUrl = { url: `${origin}/bucket/object?signature=secret-signature`, headers: {} };
|
||||
});
|
||||
describe('signed URL uploader', () => {
|
||||
describe('uploadWithSignedUrl', () => {
|
||||
it('should throw an error if upload fails', async () => {
|
||||
const fetchMock = jest.mocked(fetch);
|
||||
const res = {
|
||||
ok: false,
|
||||
status: 500,
|
||||
} as Response;
|
||||
|
||||
afterEach(async () => {
|
||||
server.closeAllConnections();
|
||||
await new Promise<void>(resolve => server.close(() => resolve()));
|
||||
});
|
||||
const key = 'cat.jpg';
|
||||
|
||||
describe(uploadWithSignedUrl, () => {
|
||||
it('sends the full body on every attempt', async () => {
|
||||
const bodies: string[] = [];
|
||||
handle = async (request, response) => {
|
||||
bodies.push(await readBodyAsync(request));
|
||||
response.statusCode = bodies.length === 1 ? 503 : 200;
|
||||
response.end();
|
||||
};
|
||||
const payload = '*'.repeat(128 * 1024);
|
||||
const signedUrl = createSignedUrl(key, 'image/jpeg');
|
||||
|
||||
await uploadWithSignedUrl(
|
||||
{ signedUrl, srcGeneratorAsync: async () => Readable.from([Buffer.from(payload)]) },
|
||||
{ retryIntervalMs: 0 }
|
||||
);
|
||||
|
||||
expect(bodies).toEqual([payload, payload]);
|
||||
});
|
||||
|
||||
it('waits longer after each failed attempt', async () => {
|
||||
respondWithStatuses([503, 503, 200]);
|
||||
const startedAt = performance.now();
|
||||
|
||||
await uploadWithSignedUrl(
|
||||
{ signedUrl, srcGeneratorAsync: async () => Readable.from(['body']) },
|
||||
{ retryIntervalMs: 100 }
|
||||
);
|
||||
|
||||
expect(requests).toBe(3);
|
||||
expect(performance.now() - startedAt).toBeGreaterThanOrEqual(290);
|
||||
});
|
||||
|
||||
it('does not retry a client error', async () => {
|
||||
handle = async (request, response) => {
|
||||
await readBodyAsync(request);
|
||||
response.statusCode = 403;
|
||||
response.end('<Error><Code>AccessDenied</Code></Error>');
|
||||
};
|
||||
|
||||
const error = await uploadWithSignedUrl(
|
||||
{ signedUrl, srcGeneratorAsync: async () => Readable.from(['body']) },
|
||||
{ retryIntervalMs: 0 }
|
||||
).catch(err => err);
|
||||
|
||||
expect(error).toBeInstanceOf(SignedUrlUploadApiError);
|
||||
expect(error).toMatchObject({
|
||||
status: 403,
|
||||
reason: 'HTTP 403 Forbidden',
|
||||
message: 'Signed URL upload failed: HTTP 403 Forbidden.',
|
||||
});
|
||||
expect(requests).toBe(1);
|
||||
});
|
||||
|
||||
it('retries a dropped connection', async () => {
|
||||
handle = async (request, response) => {
|
||||
if (requests === 1) {
|
||||
request.socket.destroy();
|
||||
return;
|
||||
}
|
||||
await readBodyAsync(request);
|
||||
response.end();
|
||||
};
|
||||
const srcGeneratorAsync = jest.fn(async () => Readable.from(['body']));
|
||||
|
||||
await uploadWithSignedUrl({ signedUrl, srcGeneratorAsync }, { retryIntervalMs: 0 });
|
||||
|
||||
expect(srcGeneratorAsync).toHaveBeenCalledTimes(2);
|
||||
expect(requests).toBe(2);
|
||||
});
|
||||
|
||||
it('retries a refused connection and keeps the signed URL out of the error', async () => {
|
||||
await new Promise<void>(resolve => server.close(() => resolve()));
|
||||
const srcGeneratorAsync = jest.fn(async () => Readable.from(['body']));
|
||||
|
||||
const error = await uploadWithSignedUrl(
|
||||
{ signedUrl, srcGeneratorAsync },
|
||||
{ retries: 1, retryIntervalMs: 0 }
|
||||
).catch(err => err);
|
||||
|
||||
expect(error).toBeInstanceOf(SignedUrlUploadNetworkError);
|
||||
expect(error).toMatchObject({ code: 'ECONNREFUSED' });
|
||||
expect(error.message).not.toContain('secret-signature');
|
||||
expect(srcGeneratorAsync).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it('does not retry a network error outside the retryable codes', async () => {
|
||||
const srcGeneratorAsync = jest.fn(async () => Readable.from(['body']));
|
||||
|
||||
const error = await uploadWithSignedUrl(
|
||||
{
|
||||
signedUrl: { ...signedUrl, url: signedUrl.url.replace('http:', 'https:') },
|
||||
srcGeneratorAsync,
|
||||
},
|
||||
{ retryIntervalMs: 0 }
|
||||
).catch(err => err);
|
||||
|
||||
expect(error).toBeInstanceOf(SignedUrlUploadNetworkError);
|
||||
expect(error).toMatchObject({ code: 'EPROTO' });
|
||||
expect(srcGeneratorAsync).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('propagates a source error without retrying, even with a retryable code', async () => {
|
||||
const sourceError = Object.assign(new Error('read failed'), { code: 'ECONNRESET' });
|
||||
const srcGeneratorAsync = jest.fn(async (): Promise<Readable> => {
|
||||
throw sourceError;
|
||||
fetchMock.mockImplementation(async () => res);
|
||||
await expect(
|
||||
uploadWithSignedUrl({
|
||||
signedUrl,
|
||||
srcGeneratorAsync: async () => Readable.from('image'),
|
||||
retryIntervalMs: 0,
|
||||
})
|
||||
).rejects.toThrow();
|
||||
});
|
||||
|
||||
await expect(
|
||||
uploadWithSignedUrl({ signedUrl, srcGeneratorAsync }, { retryIntervalMs: 0 })
|
||||
).rejects.toBe(sourceError);
|
||||
expect(srcGeneratorAsync).toHaveBeenCalledTimes(1);
|
||||
expect(requests).toBe(0);
|
||||
});
|
||||
it('should return stripped URL if successful', async () => {
|
||||
const fetchMock = jest.mocked(fetch);
|
||||
const res = {
|
||||
ok: true,
|
||||
status: 200,
|
||||
} as Response;
|
||||
|
||||
it('retries an attempt that stalls past the stall timeout', async () => {
|
||||
handle = async (request, response) => {
|
||||
await readBodyAsync(request);
|
||||
if (requests > 1) {
|
||||
response.end();
|
||||
const key = 'cat.jpg';
|
||||
|
||||
const signedUrl = createSignedUrl(key, 'image/jpeg');
|
||||
|
||||
fetchMock.mockImplementation(async () => res);
|
||||
const result = await uploadWithSignedUrl({
|
||||
signedUrl,
|
||||
srcGeneratorAsync: async () => Readable.from('image'),
|
||||
retryIntervalMs: 0,
|
||||
});
|
||||
expect(result).toEqual('https://storage.googleapis.com/turtle-v2-test/cat.jpg');
|
||||
});
|
||||
|
||||
it('should retry upload on DNS error up to 2 times', async () => {
|
||||
const fetchMock = jest.mocked(fetch);
|
||||
const res = {
|
||||
ok: true,
|
||||
status: 200,
|
||||
} as Response;
|
||||
|
||||
const key = 'cat.jpg';
|
||||
|
||||
const signedUrl = createSignedUrl(key, 'image/jpeg');
|
||||
|
||||
fetchMock
|
||||
.mockImplementationOnce(async () => {
|
||||
throw new DNSError('failed once');
|
||||
})
|
||||
.mockImplementationOnce(async () => {
|
||||
throw new DNSError('failed twice', 'EAI_AGAIN');
|
||||
})
|
||||
.mockImplementation(async () => res);
|
||||
const result = await uploadWithSignedUrl({
|
||||
signedUrl,
|
||||
srcGeneratorAsync: async () => Readable.from('image'),
|
||||
retryIntervalMs: 0,
|
||||
});
|
||||
expect(result).toEqual('https://storage.googleapis.com/turtle-v2-test/cat.jpg');
|
||||
expect(fetchMock).toHaveBeenCalledTimes(3);
|
||||
});
|
||||
|
||||
it('should retry upload on retriable status codes error up to 2 times', async () => {
|
||||
const fetchMock = jest.mocked(fetch);
|
||||
const res = {
|
||||
ok: true,
|
||||
status: 200,
|
||||
} as Response;
|
||||
|
||||
const key = 'cat.jpg';
|
||||
|
||||
const signedUrl = createSignedUrl(key, 'image/jpeg');
|
||||
|
||||
fetchMock
|
||||
.mockImplementationOnce(async () => {
|
||||
return {
|
||||
ok: false,
|
||||
status: 503,
|
||||
} as Response;
|
||||
})
|
||||
.mockImplementationOnce(async () => {
|
||||
return {
|
||||
ok: false,
|
||||
status: 408,
|
||||
} as Response;
|
||||
})
|
||||
.mockImplementation(async () => res);
|
||||
const result = await uploadWithSignedUrl({
|
||||
signedUrl,
|
||||
srcGeneratorAsync: async () => Readable.from('image'),
|
||||
retryIntervalMs: 0,
|
||||
});
|
||||
expect(result).toEqual('https://storage.googleapis.com/turtle-v2-test/cat.jpg');
|
||||
expect(fetchMock).toHaveBeenCalledTimes(3);
|
||||
});
|
||||
|
||||
it('should retry upload on retriable status codes and DNS errors error up to 2 times', async () => {
|
||||
const fetchMock = jest.mocked(fetch);
|
||||
const res = {
|
||||
ok: true,
|
||||
status: 200,
|
||||
} as Response;
|
||||
|
||||
const key = 'cat.jpg';
|
||||
|
||||
const signedUrl = createSignedUrl(key, 'image/jpeg');
|
||||
|
||||
fetchMock
|
||||
.mockImplementationOnce(async () => {
|
||||
return {
|
||||
ok: false,
|
||||
status: 503,
|
||||
} as Response;
|
||||
})
|
||||
.mockImplementationOnce(async () => {
|
||||
throw new DNSError('failed once');
|
||||
})
|
||||
.mockImplementation(async () => res);
|
||||
const result = await uploadWithSignedUrl({
|
||||
signedUrl,
|
||||
srcGeneratorAsync: async () => Readable.from('image'),
|
||||
retryIntervalMs: 0,
|
||||
});
|
||||
expect(result).toEqual('https://storage.googleapis.com/turtle-v2-test/cat.jpg');
|
||||
expect(fetchMock).toHaveBeenCalledTimes(3);
|
||||
});
|
||||
|
||||
it('should not retry upload on DNS error more than 2 times', async () => {
|
||||
const fetchMock = jest.mocked(fetch);
|
||||
const res = {
|
||||
ok: true,
|
||||
status: 200,
|
||||
} as Response;
|
||||
|
||||
const key = 'cat.jpg';
|
||||
|
||||
const signedUrl = createSignedUrl(key, 'image/jpeg');
|
||||
|
||||
const lastDNSError = new DNSError('failed thrice');
|
||||
fetchMock
|
||||
.mockImplementationOnce(async () => {
|
||||
throw new DNSError('failed once');
|
||||
})
|
||||
.mockImplementationOnce(async () => {
|
||||
throw new DNSError('failed twice', 'EAI_AGAIN');
|
||||
})
|
||||
.mockImplementationOnce(async () => {
|
||||
throw lastDNSError;
|
||||
})
|
||||
.mockImplementation(async () => res);
|
||||
await expect(
|
||||
uploadWithSignedUrl({
|
||||
signedUrl,
|
||||
srcGeneratorAsync: async () => Readable.from('image'),
|
||||
retryIntervalMs: 0,
|
||||
})
|
||||
).rejects.toThrow(lastDNSError);
|
||||
expect(fetchMock).toHaveBeenCalledTimes(3);
|
||||
});
|
||||
|
||||
it('should not retry upload on retriable status code more than 2 times', async () => {
|
||||
const fetchMock = jest.mocked(fetch);
|
||||
const res = {
|
||||
ok: true,
|
||||
status: 200,
|
||||
} as Response;
|
||||
|
||||
const key = 'cat.jpg';
|
||||
|
||||
const signedUrl = createSignedUrl(key, 'image/jpeg');
|
||||
|
||||
fetchMock
|
||||
.mockImplementationOnce(async () => {
|
||||
return {
|
||||
ok: false,
|
||||
status: 503,
|
||||
} as Response;
|
||||
})
|
||||
.mockImplementationOnce(async () => {
|
||||
return {
|
||||
ok: false,
|
||||
status: 408,
|
||||
} as Response;
|
||||
})
|
||||
.mockImplementationOnce(async () => {
|
||||
return {
|
||||
ok: false,
|
||||
status: 504,
|
||||
} as Response;
|
||||
})
|
||||
.mockImplementation(async () => res);
|
||||
await expect(
|
||||
uploadWithSignedUrl({
|
||||
signedUrl,
|
||||
srcGeneratorAsync: async () => Readable.from('image'),
|
||||
retryIntervalMs: 0,
|
||||
})
|
||||
).rejects.toThrow();
|
||||
expect(fetchMock).toHaveBeenCalledTimes(3);
|
||||
});
|
||||
|
||||
it('should not retry upload on other error codes', async () => {
|
||||
const fetchMock = jest.mocked(fetch);
|
||||
const res = {
|
||||
ok: true,
|
||||
status: 200,
|
||||
} as Response;
|
||||
|
||||
const key = 'cat.jpg';
|
||||
|
||||
const signedUrl = createSignedUrl(key, 'image/jpeg');
|
||||
|
||||
const nonDNSError = new ErrorWithCode('failed once', 'A_DIFFERENT_CODE');
|
||||
fetchMock
|
||||
.mockImplementationOnce(async () => {
|
||||
throw nonDNSError;
|
||||
})
|
||||
.mockImplementation(async () => res);
|
||||
await expect(
|
||||
uploadWithSignedUrl({
|
||||
signedUrl,
|
||||
srcGeneratorAsync: async () => Readable.from('image'),
|
||||
retryIntervalMs: 0,
|
||||
})
|
||||
).rejects.toThrow(nonDNSError);
|
||||
expect(fetchMock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('retries upload with full stream', async () => {
|
||||
const fetchMock = jest.mocked(fetch);
|
||||
const receivedBodies: Buffer[] = [];
|
||||
const recordBody = async (request: RequestInit | undefined): Promise<void> => {
|
||||
let body = Buffer.from([]);
|
||||
for await (const chunk of request!.body as Readable) {
|
||||
body = Buffer.concat([body, chunk]);
|
||||
}
|
||||
receivedBodies.push(body);
|
||||
};
|
||||
fetchMock
|
||||
.mockImplementationOnce(async (_path, request) => {
|
||||
await recordBody(request);
|
||||
return {
|
||||
ok: false,
|
||||
status: 503,
|
||||
} as Response;
|
||||
})
|
||||
.mockImplementationOnce(async (_path, request) => {
|
||||
await recordBody(request);
|
||||
throw new DNSError('failed once');
|
||||
})
|
||||
.mockImplementation(async (_path, request) => {
|
||||
await recordBody(request);
|
||||
return {
|
||||
ok: true,
|
||||
status: 201,
|
||||
} as Response;
|
||||
});
|
||||
|
||||
const signedUrl = createSignedUrl('text.txt', 'text/plain');
|
||||
|
||||
const bufferToUpload = randomBytes(16);
|
||||
|
||||
const result = await uploadWithSignedUrl({
|
||||
signedUrl,
|
||||
srcGeneratorAsync: async () => Readable.from(bufferToUpload),
|
||||
retryIntervalMs: 0,
|
||||
});
|
||||
expect(result).toEqual('https://storage.googleapis.com/turtle-v2-test/text.txt');
|
||||
expect(fetchMock).toHaveBeenCalledTimes(3);
|
||||
expect(receivedBodies.length).toBe(3);
|
||||
for (const body of receivedBodies) {
|
||||
// Here we're testing that each body received in every retry is the same.
|
||||
// If we passed the same body instance to each of the retries
|
||||
// each retry would consume a chunk of the same stream,
|
||||
// causing the uploaded file to be corrupted (missing first bytes).
|
||||
expect(body).toStrictEqual(bufferToUpload);
|
||||
}
|
||||
};
|
||||
const streams: Readable[] = [];
|
||||
const srcGeneratorAsync = async (): Promise<Readable> => {
|
||||
const stream = Readable.from(['body']);
|
||||
streams.push(stream);
|
||||
return stream;
|
||||
};
|
||||
|
||||
await uploadWithSignedUrl(
|
||||
{ signedUrl, srcGeneratorAsync },
|
||||
{ retryIntervalMs: 0, stallTimeoutMs: 100 }
|
||||
);
|
||||
|
||||
expect(requests).toBe(2);
|
||||
expect(streams.every(stream => stream.destroyed)).toBe(true);
|
||||
});
|
||||
|
||||
it('stops waiting for the next attempt when aborted', async () => {
|
||||
const controller = new AbortController();
|
||||
handle = async (request, response) => {
|
||||
await readBodyAsync(request);
|
||||
response.statusCode = 503;
|
||||
response.end();
|
||||
setTimeout(() => controller.abort(), 50);
|
||||
};
|
||||
|
||||
await expect(
|
||||
uploadWithSignedUrl(
|
||||
{ signedUrl, srcGeneratorAsync: async () => Readable.from(['body']) },
|
||||
{ signal: controller.signal, retryIntervalMs: 60_000 }
|
||||
)
|
||||
).rejects.toMatchObject({ name: 'AbortError' });
|
||||
expect(requests).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe(uploadContentWithSignedUrl, () => {
|
||||
let received: IncomingMessage | undefined;
|
||||
let body: string;
|
||||
|
||||
beforeEach(() => {
|
||||
received = undefined;
|
||||
handle = async (request, response) => {
|
||||
received = request;
|
||||
body = await readBodyAsync(request);
|
||||
response.end();
|
||||
};
|
||||
});
|
||||
|
||||
it('counts the content length in bytes', async () => {
|
||||
await uploadContentWithSignedUrl({ signedUrl, content: 'zażółć' });
|
||||
|
||||
expect(received?.headersDistinct['content-length']).toEqual(['10']);
|
||||
expect(body).toBe('zażółć');
|
||||
});
|
||||
|
||||
it('keeps a content length that the signed headers already set', async () => {
|
||||
signedUrl.headers = { 'Content-Length': '5' };
|
||||
|
||||
await uploadContentWithSignedUrl({ signedUrl, content: 'hello' });
|
||||
|
||||
expect(received?.headersDistinct['content-length']).toEqual(['5']);
|
||||
});
|
||||
|
||||
it('labels only string content as plain text', async () => {
|
||||
await uploadContentWithSignedUrl({ signedUrl, content: 'hello' });
|
||||
expect(received?.headersDistinct['content-type']).toEqual(['text/plain;charset=UTF-8']);
|
||||
|
||||
await uploadContentWithSignedUrl({ signedUrl, content: Buffer.from('hello') });
|
||||
expect(received?.headersDistinct['content-type']).toBeUndefined();
|
||||
});
|
||||
|
||||
it('keeps a content type that the signed headers already set', async () => {
|
||||
signedUrl.headers = { 'Content-Type': 'application/json' };
|
||||
|
||||
await uploadContentWithSignedUrl({ signedUrl, content: '{}' });
|
||||
|
||||
expect(received?.headersDistinct['content-type']).toEqual(['application/json']);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,70 @@
|
||||
import { Response } from 'node-fetch';
|
||||
|
||||
type RetryOptions = {
|
||||
retries: number;
|
||||
retryIntervalMs: number;
|
||||
shouldRetryOnError: (error: unknown) => boolean;
|
||||
shouldRetryOnResponse: (response: Response) => boolean;
|
||||
};
|
||||
|
||||
// based on https://github.com/googleapis/nodejs-storage/blob/8ab50804fc7bae3bbd159bbb4adf65c02215b11b/src/storage.ts#L284-L320
|
||||
export async function retryOnUploadFailure(
|
||||
fn: (attemptCount: number) => Promise<Response>,
|
||||
{
|
||||
retries,
|
||||
retryIntervalMs,
|
||||
}: { retries: RetryOptions['retries']; retryIntervalMs: RetryOptions['retryIntervalMs'] }
|
||||
): Promise<Response> {
|
||||
return await retry(fn, {
|
||||
retries,
|
||||
retryIntervalMs,
|
||||
shouldRetryOnError: err => {
|
||||
return (
|
||||
isErrorWithCode(err) &&
|
||||
(err.code === 'ENOTFOUND' ||
|
||||
err.code === 'EAI_AGAIN' ||
|
||||
err.code === 'ECONNRESET' ||
|
||||
err.code === 'ETIMEDOUT' ||
|
||||
err.code === 'EPIPE')
|
||||
);
|
||||
},
|
||||
shouldRetryOnResponse: resp => {
|
||||
return [408, 429, 500, 502, 503, 504].includes(resp.status);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function isErrorWithCode(error: unknown): error is { code: string } {
|
||||
return typeof error === 'object' && error !== null && 'code' in error;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper used to execute an inner function and possibly retry it if it throws an error
|
||||
* @param fn Function to be executed and retried in case of error
|
||||
* @param retries How many times at most should the function be retried
|
||||
* @param retryIntervalMs Time interval between the retries
|
||||
* @param shouldRetryOnError Function that determines if the function should be retried based on the error thrown
|
||||
* @param shouldRetryOnResponse Function that determines if the function should be retried based on the response
|
||||
*/
|
||||
async function retry(
|
||||
fn: (attemptCount: number) => Promise<Response>,
|
||||
{ retries, retryIntervalMs, shouldRetryOnError, shouldRetryOnResponse }: RetryOptions
|
||||
): Promise<Response> {
|
||||
let attemptCount = -1;
|
||||
for (;;) {
|
||||
try {
|
||||
attemptCount += 1;
|
||||
const resp = await fn(attemptCount);
|
||||
if (attemptCount < retries && shouldRetryOnResponse(resp)) {
|
||||
await new Promise(res => setTimeout(res, retryIntervalMs));
|
||||
} else {
|
||||
return resp;
|
||||
}
|
||||
} catch (err: unknown) {
|
||||
if (attemptCount === retries || !shouldRetryOnError(err)) {
|
||||
throw err;
|
||||
}
|
||||
await new Promise(res => setTimeout(res, retryIntervalMs));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,8 @@
|
||||
import { SystemError } from '@expo/eas-build-job';
|
||||
import fetch, { FetchError } from 'node-fetch';
|
||||
import { Readable } from 'node:stream';
|
||||
import { finished } from 'node:stream/promises';
|
||||
import { setTimeout as delay } from 'node:timers/promises';
|
||||
import { Readable } from 'stream';
|
||||
import { URL } from 'url';
|
||||
|
||||
import { AbortUtils } from '../utils/AbortUtils';
|
||||
import { isConnectFailure, isConnectionInterruptedError } from '../utils/networkErrors';
|
||||
import { retryOnUploadFailure } from './retry';
|
||||
|
||||
export type SignedUrl = {
|
||||
url: string;
|
||||
@@ -15,140 +12,46 @@ export type SignedUrl = {
|
||||
export type UploadWithSignedUrlParams = {
|
||||
signedUrl: SignedUrl;
|
||||
srcGeneratorAsync: () => Promise<Readable>;
|
||||
};
|
||||
|
||||
export type UploadWithSignedUrlOptions = {
|
||||
signal?: AbortSignal;
|
||||
retries?: number;
|
||||
retryIntervalMs?: number;
|
||||
stallTimeoutMs?: number;
|
||||
retries?: number;
|
||||
};
|
||||
|
||||
export async function uploadWithSignedUrl(
|
||||
{ signedUrl, srcGeneratorAsync }: UploadWithSignedUrlParams,
|
||||
{
|
||||
signal,
|
||||
retries = 2,
|
||||
retryIntervalMs = 30_000,
|
||||
stallTimeoutMs = 90_000,
|
||||
}: UploadWithSignedUrlOptions = {}
|
||||
): Promise<void> {
|
||||
for (let attempt = 0; ; attempt++) {
|
||||
signal?.throwIfAborted();
|
||||
const stream = await srcGeneratorAsync();
|
||||
// Upload time scales with file size, so the deadline bounds stalls, not the whole request.
|
||||
const timeout = AbortUtils.resettableTimeout(stallTimeoutMs);
|
||||
try {
|
||||
await putAsync({
|
||||
signedUrl,
|
||||
stream,
|
||||
signal: signal ? AbortSignal.any([signal, timeout.signal]) : timeout.signal,
|
||||
onProgress: timeout.resetDeadline,
|
||||
});
|
||||
return;
|
||||
} catch (error) {
|
||||
signal?.throwIfAborted();
|
||||
if (attempt >= retries || (!timeout.signal.aborted && !isRetryableError(error))) {
|
||||
throw error;
|
||||
}
|
||||
await delay(retryIntervalMs * (attempt + 1), undefined, { signal });
|
||||
} finally {
|
||||
stream.destroy();
|
||||
await finished(stream).catch(() => {});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function uploadContentWithSignedUrl(
|
||||
{ signedUrl, content }: { signedUrl: SignedUrl; content: string | Buffer },
|
||||
options?: UploadWithSignedUrlOptions
|
||||
): Promise<void> {
|
||||
const headers = { ...signedUrl.headers };
|
||||
const headerNames = Object.keys(headers).map(name => name.toLowerCase());
|
||||
if (!headerNames.includes('content-length')) {
|
||||
headers['content-length'] = String(Buffer.byteLength(content));
|
||||
}
|
||||
if (typeof content === 'string' && !headerNames.includes('content-type')) {
|
||||
headers['content-type'] = 'text/plain;charset=UTF-8';
|
||||
}
|
||||
await uploadWithSignedUrl(
|
||||
{
|
||||
signedUrl: { url: signedUrl.url, headers },
|
||||
srcGeneratorAsync: async () => Readable.from([content]),
|
||||
},
|
||||
options
|
||||
);
|
||||
}
|
||||
|
||||
const RETRYABLE_STATUSES = [408, 429, 500, 502, 503, 504];
|
||||
function isRetryableError(error: unknown): boolean {
|
||||
if (error instanceof SignedUrlUploadApiError && RETRYABLE_STATUSES.includes(error.status)) {
|
||||
return true;
|
||||
}
|
||||
return isConnectFailure(error) || isConnectionInterruptedError(error);
|
||||
}
|
||||
|
||||
async function putAsync({
|
||||
export async function uploadWithSignedUrl({
|
||||
signedUrl,
|
||||
stream,
|
||||
signal,
|
||||
onProgress,
|
||||
}: {
|
||||
signedUrl: SignedUrl;
|
||||
stream: Readable;
|
||||
signal: AbortSignal;
|
||||
onProgress: () => void;
|
||||
}): Promise<void> {
|
||||
const requestController = new AbortController();
|
||||
srcGeneratorAsync,
|
||||
retries = 2,
|
||||
retryIntervalMs = 30_000,
|
||||
}: UploadWithSignedUrlParams): Promise<string> {
|
||||
let resp;
|
||||
try {
|
||||
const responsePromise = fetch(signedUrl.url, {
|
||||
method: 'PUT',
|
||||
headers: signedUrl.headers,
|
||||
body: stream,
|
||||
signal: AbortSignal.any([signal, requestController.signal]),
|
||||
});
|
||||
// fetch has piped the body already, so this listener observes flow without starting it.
|
||||
stream.on('data', onProgress);
|
||||
|
||||
const response = await responsePromise;
|
||||
signal.throwIfAborted();
|
||||
if (!response.ok) {
|
||||
throw new SignedUrlUploadApiError(response);
|
||||
resp = await retryOnUploadFailure(
|
||||
async () => {
|
||||
const src = await srcGeneratorAsync();
|
||||
return await fetch(signedUrl.url, {
|
||||
method: 'PUT',
|
||||
headers: signedUrl.headers,
|
||||
body: src,
|
||||
});
|
||||
},
|
||||
{ retries, retryIntervalMs }
|
||||
);
|
||||
} catch (err: unknown) {
|
||||
if (err instanceof FetchError) {
|
||||
throw new Error(`Failed to upload the file, reason: ${err.code}`);
|
||||
}
|
||||
} catch (error) {
|
||||
signal.throwIfAborted();
|
||||
if (error instanceof FetchError) {
|
||||
throw new SignedUrlUploadNetworkError(error);
|
||||
}
|
||||
throw error;
|
||||
} finally {
|
||||
// node-fetch destroys the request body on abort, and its response stream is a PassThrough
|
||||
// whose destruction leaves the socket open, so the request itself is aborted here.
|
||||
requestController.abort();
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
export class SignedUrlUploadApiError extends SystemError {
|
||||
readonly status: number;
|
||||
readonly reason: string;
|
||||
|
||||
constructor(response: { status: number; statusText: string }) {
|
||||
const reason = `HTTP ${response.status} ${response.statusText}`;
|
||||
super(`Signed URL upload failed: ${reason}.`);
|
||||
this.status = response.status;
|
||||
this.reason = reason;
|
||||
if (!resp.ok) {
|
||||
let body: string | undefined;
|
||||
try {
|
||||
body = await resp.text();
|
||||
} catch {}
|
||||
throw new Error(
|
||||
`Failed to upload file: status: ${resp.status} status text: ${resp.statusText}, body: ${body}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// node-fetch names the request URL in its message, which we want to avoid for signed urls.
|
||||
export class SignedUrlUploadNetworkError extends SystemError {
|
||||
readonly code: string | undefined;
|
||||
readonly reason: string;
|
||||
|
||||
constructor(error: FetchError) {
|
||||
const reason = error.code ?? 'network error';
|
||||
super(`Signed URL upload failed: ${reason}.`);
|
||||
this.code = error.code;
|
||||
this.reason = reason;
|
||||
}
|
||||
const url = new URL(signedUrl.url);
|
||||
return `${url.protocol}//${url.host}${url.pathname}`;
|
||||
}
|
||||
@@ -154,9 +154,8 @@ export namespace AndroidEmulatorUtils {
|
||||
try {
|
||||
let configIniFileContent = await fs.promises.readFile(configIniFile, 'utf-8');
|
||||
|
||||
// Keep the lower RAM default for older images; newer guests need more headroom for ADB.
|
||||
const apiLevel = systemImagePackage.match(/^system-images;android-(\d+)/)?.[1];
|
||||
const ramSize = !env.DEVICE_RUN_SESSION_ID && apiLevel && Number(apiLevel) < 35 ? 2048 : 4096;
|
||||
// Give interactive sessions more headroom for guest VSOCK packet allocations.
|
||||
const ramSize = env.DEVICE_RUN_SESSION_ID ? 4096 : 2048;
|
||||
logger.info(`Setting hw.ramSize to ${ramSize}.`);
|
||||
configIniFileContent = `${configIniFileContent}\nhw.ramSize=${ramSize}\n`;
|
||||
|
||||
|
||||
@@ -121,7 +121,7 @@ describe('AndroidEmulatorUtils', () => {
|
||||
'hw.lcd.density=420',
|
||||
'skin.path=pixel_9',
|
||||
'showDeviceFrame=yes',
|
||||
'hw.ramSize=4096',
|
||||
'hw.ramSize=2048',
|
||||
'',
|
||||
'hw.lcd.density=220',
|
||||
'',
|
||||
|
||||
@@ -20,7 +20,6 @@ it.each([
|
||||
'ENETDOWN',
|
||||
'ENETUNREACH',
|
||||
'ENOTFOUND',
|
||||
'ERR_SOCKET_CONNECTION_TIMEOUT',
|
||||
])('recognizes wrapped %s as a connection establishment failure', code => {
|
||||
const error = new Error('Request failed', {
|
||||
cause: new TypeError('fetch failed', {
|
||||
@@ -31,7 +30,7 @@ it.each([
|
||||
expect(isConnectionInterruptedError(error)).toBe(false);
|
||||
});
|
||||
|
||||
it.each(['ECONNRESET', 'EPIPE', 'ETIMEDOUT', 'UND_ERR_SOCKET'])(
|
||||
it.each(['ECONNRESET', 'UND_ERR_SOCKET'])(
|
||||
'requires caller-specific recovery for wrapped %s',
|
||||
code => {
|
||||
const error = new Error('Request failed', {
|
||||
@@ -42,7 +41,7 @@ it.each(['ECONNRESET', 'EPIPE', 'ETIMEDOUT', 'UND_ERR_SOCKET'])(
|
||||
}
|
||||
);
|
||||
|
||||
it.each(['CERT_HAS_EXPIRED', undefined])('does not automatically retry %s', code => {
|
||||
it.each(['ETIMEDOUT', 'CERT_HAS_EXPIRED', undefined])('does not automatically retry %s', code => {
|
||||
const error = Object.assign(new Error('Request failed'), { code });
|
||||
expect(isConnectFailure(error)).toBe(false);
|
||||
expect(isConnectionInterruptedError(error)).toBe(false);
|
||||
|
||||
@@ -8,14 +8,6 @@ const CONNECT_FAILURE_CODES = new Set([
|
||||
'ENETDOWN',
|
||||
'ENETUNREACH',
|
||||
'ENOTFOUND',
|
||||
'ERR_SOCKET_CONNECTION_TIMEOUT',
|
||||
]);
|
||||
|
||||
const CONNECTION_INTERRUPTED_CODES = new Set([
|
||||
'ECONNRESET',
|
||||
'EPIPE',
|
||||
'ETIMEDOUT',
|
||||
'UND_ERR_SOCKET',
|
||||
]);
|
||||
|
||||
export function isConnectFailure(error: unknown): boolean {
|
||||
@@ -34,11 +26,7 @@ export function isConnectFailure(error: unknown): boolean {
|
||||
// The server may already have applied a request. The caller must decide whether repetition is safe.
|
||||
export function isConnectionInterruptedError(error: unknown): boolean {
|
||||
for (let current = error; current instanceof Error; current = current.cause) {
|
||||
if (
|
||||
'code' in current &&
|
||||
typeof current.code === 'string' &&
|
||||
CONNECTION_INTERRUPTED_CODES.has(current.code)
|
||||
) {
|
||||
if ('code' in current && (current.code === 'ECONNRESET' || current.code === 'UND_ERR_SOCKET')) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -871,7 +871,7 @@ describe('StaticWorkflowInterpolationContextZ', () => {
|
||||
});
|
||||
|
||||
describe('GitHub context event payload passthrough', () => {
|
||||
it('preserves webhook fields and validates documented pull request details', () => {
|
||||
it('accepts arbitrary fields at all event levels', () => {
|
||||
const context = {
|
||||
after: {},
|
||||
needs: {},
|
||||
@@ -895,19 +895,7 @@ describe('GitHub context event payload passthrough', () => {
|
||||
},
|
||||
pull_request: {
|
||||
number: 123,
|
||||
title: 'Fix the build',
|
||||
body: null,
|
||||
state: 'open',
|
||||
draft: true,
|
||||
merged: false,
|
||||
html_url: 'https://github.com/expo/expo/pull/123',
|
||||
user: { login: 'octocat', id: 1 },
|
||||
labels: [{ name: 'bug', color: 'red' }],
|
||||
head: { ref: 'fix-build', sha: 'abc123', repo: { name: 'expo' } },
|
||||
base: { ref: 'main', sha: 'def456' },
|
||||
created_at: '2026-10-01T00:00:00Z',
|
||||
updated_at: '2026-10-02T00:00:00Z',
|
||||
merged_at: null,
|
||||
},
|
||||
number: 123,
|
||||
schedule: '0 0 * * *',
|
||||
@@ -933,34 +921,6 @@ describe('GitHub context event payload passthrough', () => {
|
||||
const parsed = StaticWorkflowInterpolationContextZ.parse(context);
|
||||
|
||||
expect(parsed.github?.event).toMatchObject(context.github.event);
|
||||
expect(() =>
|
||||
StaticWorkflowInterpolationContextZ.parse({
|
||||
...context,
|
||||
github: {
|
||||
...context.github,
|
||||
event: {
|
||||
...context.github.event,
|
||||
pull_request: { ...context.github.event.pull_request, title: 123 },
|
||||
},
|
||||
},
|
||||
})
|
||||
).toThrow();
|
||||
});
|
||||
|
||||
it.each([
|
||||
{ number: 123 },
|
||||
{ number: 123, body: null, user: null, merged: null, merged_at: null },
|
||||
])('accepts partial pull request payloads: %j', pullRequest => {
|
||||
const github = {
|
||||
event_name: 'pull_request',
|
||||
sha: 'abc123',
|
||||
ref: 'refs/heads/main',
|
||||
ref_name: 'main',
|
||||
ref_type: 'branch',
|
||||
event: { pull_request: pullRequest },
|
||||
};
|
||||
|
||||
expect(StaticWorkflowInterpolationContextZ.shape.github.parse(github)).toEqual(github);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -7,3 +7,24 @@ export enum ManagedArtifactType {
|
||||
*/
|
||||
XCODE_BUILD_LOGS = 'XCODE_BUILD_LOGS',
|
||||
}
|
||||
|
||||
export enum GenericArtifactType {
|
||||
ANDROID_APK = 'android-apk',
|
||||
ANDROID_AAB = 'android-aab',
|
||||
|
||||
IOS_SIMULATOR_APP = 'ios-simulator-app',
|
||||
IOS_IPA = 'ios-ipa',
|
||||
|
||||
OTHER = 'other',
|
||||
}
|
||||
|
||||
export const isGenericArtifact = <
|
||||
TSpec extends { type: GenericArtifactType | ManagedArtifactType },
|
||||
>(
|
||||
artifactSpec: TSpec
|
||||
): artifactSpec is TSpec & { type: GenericArtifactType } => {
|
||||
if (Object.values(GenericArtifactType).includes(artifactSpec.type as GenericArtifactType)) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
@@ -207,7 +207,6 @@ const GitHubContextZ = z.object({
|
||||
.record(z.string(), z.unknown())
|
||||
.and(
|
||||
z.object({
|
||||
action: z.string().optional(),
|
||||
label: z
|
||||
.looseObject({
|
||||
name: z.string(),
|
||||
@@ -222,19 +221,6 @@ const GitHubContextZ = z.object({
|
||||
pull_request: z
|
||||
.looseObject({
|
||||
number: z.number(),
|
||||
title: z.string().optional(),
|
||||
body: z.string().nullable().optional(),
|
||||
state: z.enum(['open', 'closed']).optional(),
|
||||
draft: z.boolean().optional(),
|
||||
merged: z.boolean().nullable().optional(),
|
||||
html_url: z.string().optional(),
|
||||
user: z.looseObject({ login: z.string() }).nullable().optional(),
|
||||
labels: z.array(z.looseObject({ name: z.string() })).optional(),
|
||||
head: z.looseObject({ ref: z.string(), sha: z.string() }).optional(),
|
||||
base: z.looseObject({ ref: z.string(), sha: z.string() }).optional(),
|
||||
created_at: z.string().optional(),
|
||||
updated_at: z.string().optional(),
|
||||
merged_at: z.string().nullable().optional(),
|
||||
})
|
||||
.optional(),
|
||||
number: z.number().optional(),
|
||||
|
||||
Generated
+731
-725
File diff suppressed because it is too large.
Load diff
@@ -1,4 +1,3 @@
|
||||
import { ExpoConfig } from '@expo/config';
|
||||
import { Env, Platform } from '@expo/eas-build-job';
|
||||
import { BuildProfile, EasJson, ResourceClass } from '@expo/eas-json';
|
||||
import JsonFile from '@expo/json-file';
|
||||
@@ -14,6 +13,7 @@ import { createIosContextAsync } from './ios/build';
|
||||
import { LocalBuildMode, LocalBuildOptions } from './local';
|
||||
import { resolveBuildResourceClass } from './utils/resourceClass';
|
||||
import { Analytics, AnalyticsEventProperties, BuildEvent } from '../analytics/AnalyticsManager';
|
||||
import { DynamicConfigContextFn } from '../commandUtils/context/DynamicProjectConfigContextField';
|
||||
import { ExpoGraphqlClient } from '../commandUtils/context/contextUtils/createGraphqlClient';
|
||||
import { CredentialsContext } from '../credentials/context';
|
||||
import { LazyExperimentationClient } from '../experimentation/LazyExperimentationClient';
|
||||
@@ -40,8 +40,7 @@ export async function createBuildContextAsync<T extends Platform>({
|
||||
graphqlClient,
|
||||
analytics,
|
||||
vcsClient,
|
||||
exp,
|
||||
projectId,
|
||||
getDynamicPrivateProjectConfigAsync,
|
||||
customBuildConfigMetadata,
|
||||
buildLoggerLevel,
|
||||
freezeCredentials,
|
||||
@@ -67,8 +66,7 @@ export async function createBuildContextAsync<T extends Platform>({
|
||||
graphqlClient: ExpoGraphqlClient;
|
||||
analytics: Analytics;
|
||||
vcsClient: Client;
|
||||
exp: ExpoConfig;
|
||||
projectId: string;
|
||||
getDynamicPrivateProjectConfigAsync: DynamicConfigContextFn;
|
||||
customBuildConfigMetadata?: CustomBuildConfigMetadata;
|
||||
buildLoggerLevel?: LoggerLevel;
|
||||
freezeCredentials: boolean;
|
||||
@@ -79,6 +77,9 @@ export async function createBuildContextAsync<T extends Platform>({
|
||||
experimentation?: LazyExperimentationClient;
|
||||
authenticationInfo?: LoggedInAuthenticationInfo;
|
||||
}): Promise<BuildContext<T>> {
|
||||
const { exp, projectId } = await getDynamicPrivateProjectConfigAsync({
|
||||
env,
|
||||
});
|
||||
const projectName = exp.slug;
|
||||
|
||||
const [account, workflow] = await Promise.all([
|
||||
|
||||
@@ -119,7 +119,7 @@ export async function runBuildAndSubmitAsync({
|
||||
actor,
|
||||
getDynamicPrivateProjectConfigAsync,
|
||||
downloadSimBuildAutoConfirm,
|
||||
evaluatedConfig,
|
||||
envOverride,
|
||||
experimentation,
|
||||
authenticationInfo,
|
||||
}: {
|
||||
@@ -131,7 +131,7 @@ export async function runBuildAndSubmitAsync({
|
||||
actor: Actor;
|
||||
getDynamicPrivateProjectConfigAsync: DynamicConfigContextFn;
|
||||
downloadSimBuildAutoConfirm?: boolean;
|
||||
evaluatedConfig?: { env: Env; exp: ExpoConfig; projectId: string };
|
||||
envOverride?: Env;
|
||||
/** Enables the Free plan limit experiments on the build request error path. */
|
||||
experimentation?: LazyExperimentationClient;
|
||||
/** Lets the Free plan limit checkout experiment create a checkout session. */
|
||||
@@ -224,15 +224,15 @@ export async function runBuildAndSubmitAsync({
|
||||
for (const buildProfile of buildProfiles) {
|
||||
const platform = toAppPlatform(buildProfile.platform);
|
||||
|
||||
const { env, exp, projectId } =
|
||||
evaluatedConfig ??
|
||||
(await evaluateConfigWithEnvVarsAsync({
|
||||
buildProfile: buildProfile.profile,
|
||||
buildProfileName: buildProfile.profileName,
|
||||
graphqlClient,
|
||||
getProjectConfig: getDynamicPrivateProjectConfigAsync,
|
||||
opts: { env: buildProfile.profile.env },
|
||||
}));
|
||||
const { env } = !envOverride
|
||||
? await evaluateConfigWithEnvVarsAsync({
|
||||
buildProfile: buildProfile.profile,
|
||||
buildProfileName: buildProfile.profileName,
|
||||
graphqlClient,
|
||||
getProjectConfig: getDynamicPrivateProjectConfigAsync,
|
||||
opts: { env: buildProfile.profile.env },
|
||||
})
|
||||
: { env: envOverride };
|
||||
|
||||
const { build: maybeBuild, buildCtx } = await prepareAndStartBuildAsync({
|
||||
projectDir,
|
||||
@@ -244,8 +244,7 @@ export async function runBuildAndSubmitAsync({
|
||||
graphqlClient,
|
||||
analytics,
|
||||
vcsClient,
|
||||
exp,
|
||||
projectId,
|
||||
getDynamicPrivateProjectConfigAsync,
|
||||
customBuildConfigMetadata: customBuildConfigMetadataByPlatform[platform],
|
||||
env,
|
||||
easJsonAccessor,
|
||||
@@ -385,8 +384,7 @@ async function prepareAndStartBuildAsync({
|
||||
graphqlClient,
|
||||
analytics,
|
||||
vcsClient,
|
||||
exp,
|
||||
projectId,
|
||||
getDynamicPrivateProjectConfigAsync,
|
||||
customBuildConfigMetadata,
|
||||
env,
|
||||
easJsonAccessor,
|
||||
@@ -402,8 +400,7 @@ async function prepareAndStartBuildAsync({
|
||||
graphqlClient: ExpoGraphqlClient;
|
||||
analytics: Analytics;
|
||||
vcsClient: Client;
|
||||
exp: ExpoConfig;
|
||||
projectId: string;
|
||||
getDynamicPrivateProjectConfigAsync: DynamicConfigContextFn;
|
||||
customBuildConfigMetadata?: CustomBuildConfigMetadata;
|
||||
easJsonAccessor: EasJsonAccessor;
|
||||
env: Env;
|
||||
@@ -426,8 +423,7 @@ async function prepareAndStartBuildAsync({
|
||||
graphqlClient,
|
||||
analytics,
|
||||
vcsClient,
|
||||
exp,
|
||||
projectId,
|
||||
getDynamicPrivateProjectConfigAsync,
|
||||
customBuildConfigMetadata,
|
||||
buildLoggerLevel: flags.buildLoggerLevel ?? (Log.isDebug ? LoggerLevel.DEBUG : undefined),
|
||||
freezeCredentials: flags.freezeCredentials,
|
||||
|
||||
@@ -3,7 +3,7 @@ import { ExpoConfig } from '@expo/config';
|
||||
import ContextField, { ContextOptions } from './ContextField';
|
||||
import { createGraphqlClient } from './contextUtils/createGraphqlClient';
|
||||
import { findProjectDirAndVerifyProjectSetupAsync } from './contextUtils/findProjectDirAndVerifyProjectSetupAsync';
|
||||
import { getProjectIdAsync, isProjectIdInAppConfig } from './contextUtils/getProjectIdAsync';
|
||||
import { getProjectIdAsync } from './contextUtils/getProjectIdAsync';
|
||||
import { loadServerSideEnvironmentVariablesAsync } from './contextUtils/loadServerSideEnvironmentVariablesAsync';
|
||||
import {
|
||||
ExpoConfigOptions,
|
||||
@@ -18,57 +18,83 @@ export type DynamicConfigContextFn = (options?: ExpoConfigOptions) => Promise<{
|
||||
}>;
|
||||
|
||||
export class DynamicPublicProjectConfigContextField extends ContextField<DynamicConfigContextFn> {
|
||||
async getValueAsync(options: ContextOptions): Promise<DynamicConfigContextFn> {
|
||||
return await createDynamicConfigFnAsync(getPublicExpoConfigAsync, options);
|
||||
async getValueAsync({
|
||||
nonInteractive,
|
||||
sessionManager,
|
||||
withServerSideEnvironment,
|
||||
}: ContextOptions): Promise<DynamicConfigContextFn> {
|
||||
const projectDir = await findProjectDirAndVerifyProjectSetupAsync();
|
||||
return async (options?: ExpoConfigOptions) => {
|
||||
const expBefore = await getPublicExpoConfigAsync(projectDir, options);
|
||||
const projectId = await getProjectIdAsync(sessionManager, expBefore, {
|
||||
nonInteractive,
|
||||
env: options?.env,
|
||||
});
|
||||
if (withServerSideEnvironment) {
|
||||
const { authenticationInfo } = await sessionManager.ensureLoggedInAsync({
|
||||
nonInteractive,
|
||||
});
|
||||
const graphqlClient = createGraphqlClient(authenticationInfo);
|
||||
const serverSideEnvironmentVariables = await loadServerSideEnvironmentVariablesAsync({
|
||||
environment: withServerSideEnvironment,
|
||||
projectId,
|
||||
graphqlClient,
|
||||
});
|
||||
options = {
|
||||
...options,
|
||||
env: {
|
||||
...options?.env,
|
||||
...serverSideEnvironmentVariables,
|
||||
},
|
||||
};
|
||||
}
|
||||
const exp = await getPublicExpoConfigAsync(projectDir, options);
|
||||
return {
|
||||
exp,
|
||||
projectDir,
|
||||
projectId,
|
||||
};
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export class DynamicPrivateProjectConfigContextField extends ContextField<DynamicConfigContextFn> {
|
||||
async getValueAsync(options: ContextOptions): Promise<DynamicConfigContextFn> {
|
||||
return await createDynamicConfigFnAsync(getPrivateExpoConfigAsync, options);
|
||||
}
|
||||
}
|
||||
|
||||
async function createDynamicConfigFnAsync(
|
||||
getExpoConfigAsync: (projectDir: string, options?: ExpoConfigOptions) => Promise<ExpoConfig>,
|
||||
{ nonInteractive, sessionManager, withServerSideEnvironment }: ContextOptions
|
||||
): Promise<DynamicConfigContextFn> {
|
||||
const projectDir = await findProjectDirAndVerifyProjectSetupAsync();
|
||||
return async (options?: ExpoConfigOptions) => {
|
||||
const expBefore = await getExpoConfigAsync(projectDir, options);
|
||||
const projectId = await getProjectIdAsync(sessionManager, expBefore, {
|
||||
nonInteractive,
|
||||
env: options?.env,
|
||||
});
|
||||
if (withServerSideEnvironment) {
|
||||
const { authenticationInfo } = await sessionManager.ensureLoggedInAsync({
|
||||
async getValueAsync({
|
||||
nonInteractive,
|
||||
sessionManager,
|
||||
withServerSideEnvironment,
|
||||
}: ContextOptions): Promise<DynamicConfigContextFn> {
|
||||
const projectDir = await findProjectDirAndVerifyProjectSetupAsync();
|
||||
return async (options?: ExpoConfigOptions) => {
|
||||
const expBefore = await getPrivateExpoConfigAsync(projectDir, options);
|
||||
const projectId = await getProjectIdAsync(sessionManager, expBefore, {
|
||||
nonInteractive,
|
||||
env: options?.env,
|
||||
});
|
||||
const graphqlClient = createGraphqlClient(authenticationInfo);
|
||||
const serverSideEnvironmentVariables = await loadServerSideEnvironmentVariablesAsync({
|
||||
environment: withServerSideEnvironment,
|
||||
projectId,
|
||||
graphqlClient,
|
||||
});
|
||||
options = {
|
||||
...options,
|
||||
env: {
|
||||
...options?.env,
|
||||
...serverSideEnvironmentVariables,
|
||||
},
|
||||
};
|
||||
} else if (isProjectIdInAppConfig(expBefore, projectId)) {
|
||||
if (withServerSideEnvironment) {
|
||||
const { authenticationInfo } = await sessionManager.ensureLoggedInAsync({
|
||||
nonInteractive,
|
||||
});
|
||||
const graphqlClient = createGraphqlClient(authenticationInfo);
|
||||
const serverSideEnvironmentVariables = await loadServerSideEnvironmentVariablesAsync({
|
||||
environment: withServerSideEnvironment,
|
||||
projectId,
|
||||
graphqlClient,
|
||||
});
|
||||
options = {
|
||||
...options,
|
||||
env: {
|
||||
...options?.env,
|
||||
...serverSideEnvironmentVariables,
|
||||
},
|
||||
};
|
||||
}
|
||||
const exp = await getPrivateExpoConfigAsync(projectDir, options);
|
||||
return {
|
||||
exp: expBefore,
|
||||
exp,
|
||||
projectDir,
|
||||
projectId,
|
||||
};
|
||||
}
|
||||
const exp = await getExpoConfigAsync(projectDir, options);
|
||||
return {
|
||||
exp,
|
||||
projectDir,
|
||||
projectId,
|
||||
};
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import ContextField, { ContextOptions } from './ContextField';
|
||||
import { ExpoGraphqlClient } from './contextUtils/createGraphqlClient';
|
||||
import { ExpoGraphqlClient, createGraphqlClient } from './contextUtils/createGraphqlClient';
|
||||
import { LoggedInAuthenticationInfo } from '../../user/SessionManager';
|
||||
import { Actor } from '../../user/User';
|
||||
import FeatureGateEnvOverrides from '../gating/FeatureGateEnvOverrides';
|
||||
@@ -22,7 +22,7 @@ export default class LoggedInContextField extends ContextField<LoggedInContextTy
|
||||
});
|
||||
const featureGateServerValues: { [key: string]: boolean } = actor?.featureGates ?? {};
|
||||
|
||||
const graphqlClient = sessionManager.getGraphqlClient(authenticationInfo);
|
||||
const graphqlClient = createGraphqlClient(authenticationInfo);
|
||||
|
||||
return {
|
||||
actor,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import ContextField, { ContextOptions } from './ContextField';
|
||||
import { ExpoGraphqlClient } from './contextUtils/createGraphqlClient';
|
||||
import { ExpoGraphqlClient, createGraphqlClient } from './contextUtils/createGraphqlClient';
|
||||
import { Actor } from '../../user/User';
|
||||
import FeatureGateEnvOverrides from '../gating/FeatureGateEnvOverrides';
|
||||
import FeatureGating from '../gating/FeatureGating';
|
||||
@@ -21,7 +21,7 @@ export default class MaybeLoggedInContextField extends ContextField<MaybeLoggedI
|
||||
sessionSecret: sessionManager.getSessionSecret(),
|
||||
};
|
||||
|
||||
const graphqlClient = sessionManager.getGraphqlClient(authenticationInfo);
|
||||
const graphqlClient = createGraphqlClient(authenticationInfo);
|
||||
const actor = await sessionManager.getUserAsync();
|
||||
const featureGateServerValues: { [key: string]: boolean } = actor?.featureGates ?? {};
|
||||
|
||||
|
||||
+2
-5
@@ -4,7 +4,7 @@ import { InvalidEasJsonError } from '@expo/eas-json/build/errors';
|
||||
import ContextField, { ContextOptions } from './ContextField';
|
||||
import { createGraphqlClient } from './contextUtils/createGraphqlClient';
|
||||
import { findProjectDirAndVerifyProjectSetupAsync } from './contextUtils/findProjectDirAndVerifyProjectSetupAsync';
|
||||
import { getProjectIdAsync, isProjectIdInAppConfig } from './contextUtils/getProjectIdAsync';
|
||||
import { getProjectIdAsync } from './contextUtils/getProjectIdAsync';
|
||||
import { loadServerSideEnvironmentVariablesAsync } from './contextUtils/loadServerSideEnvironmentVariablesAsync';
|
||||
import { getPrivateExpoConfigAsync } from '../../project/expoConfig';
|
||||
|
||||
@@ -58,10 +58,7 @@ export class OptionalPrivateProjectConfigContextField extends ContextField<
|
||||
});
|
||||
serverSideEnvVars = serverSideEnvironmentVariables;
|
||||
}
|
||||
const exp =
|
||||
withServerSideEnvironment || !isProjectIdInAppConfig(expBefore, projectId)
|
||||
? await getPrivateExpoConfigAsync(projectDir, { env: serverSideEnvVars })
|
||||
: expBefore;
|
||||
const exp = await getPrivateExpoConfigAsync(projectDir, { env: serverSideEnvVars });
|
||||
return {
|
||||
exp,
|
||||
projectDir,
|
||||
|
||||
@@ -3,7 +3,7 @@ import { ExpoConfig } from '@expo/config';
|
||||
import ContextField, { ContextOptions } from './ContextField';
|
||||
import { createGraphqlClient } from './contextUtils/createGraphqlClient';
|
||||
import { findProjectDirAndVerifyProjectSetupAsync } from './contextUtils/findProjectDirAndVerifyProjectSetupAsync';
|
||||
import { getProjectIdAsync, isProjectIdInAppConfig } from './contextUtils/getProjectIdAsync';
|
||||
import { getProjectIdAsync } from './contextUtils/getProjectIdAsync';
|
||||
import { loadServerSideEnvironmentVariablesAsync } from './contextUtils/loadServerSideEnvironmentVariablesAsync';
|
||||
import { getPrivateExpoConfigAsync } from '../../project/expoConfig';
|
||||
|
||||
@@ -39,10 +39,7 @@ export class PrivateProjectConfigContextField extends ContextField<{
|
||||
});
|
||||
serverSideEnvVars = serverSideEnvironmentVariables;
|
||||
}
|
||||
const exp =
|
||||
withServerSideEnvironment || !isProjectIdInAppConfig(expBefore, projectId)
|
||||
? await getPrivateExpoConfigAsync(projectDir, { env: serverSideEnvVars })
|
||||
: expBefore;
|
||||
const exp = await getPrivateExpoConfigAsync(projectDir, { env: serverSideEnvVars });
|
||||
|
||||
return {
|
||||
projectId,
|
||||
|
||||
@@ -1,63 +0,0 @@
|
||||
import nock from 'nock';
|
||||
|
||||
import { AnalyticsWithOrchestration } from '../../../analytics/AnalyticsManager';
|
||||
import { getExpoApiBaseUrl } from '../../../api';
|
||||
import { AppQuery } from '../../../graphql/queries/AppQuery';
|
||||
import SessionManager from '../../../user/SessionManager';
|
||||
import { ContextOptions } from '../ContextField';
|
||||
import LoggedInContextField from '../LoggedInContextField';
|
||||
import { getProjectIdAsync } from '../contextUtils/getProjectIdAsync';
|
||||
|
||||
const app = {
|
||||
__typename: 'App',
|
||||
id: 'PROJECT_ID',
|
||||
name: 'app',
|
||||
fullName: '@owner/app',
|
||||
slug: 'app',
|
||||
ownerAccount: { __typename: 'Account', id: 'ACCOUNT_ID', name: 'owner' },
|
||||
githubRepository: null,
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
nock.disableNetConnect();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
nock.cleanAll();
|
||||
nock.enableNetConnect();
|
||||
});
|
||||
|
||||
describe(LoggedInContextField, () => {
|
||||
it('shares the GraphQL cache with the project ID check', async () => {
|
||||
const analytics: AnalyticsWithOrchestration = {
|
||||
logEvent: jest.fn(),
|
||||
setActor: jest.fn(),
|
||||
flushAsync: jest.fn(),
|
||||
getDeviceId: jest.fn(() => null),
|
||||
};
|
||||
const sessionManager = new SessionManager(analytics);
|
||||
jest.spyOn(sessionManager, 'ensureLoggedInAsync').mockResolvedValue({
|
||||
actor: { __typename: 'User', id: 'USER_ID', username: 'owner' } as any,
|
||||
authenticationInfo: { accessToken: 'token', sessionSecret: null },
|
||||
});
|
||||
const scope = nock(getExpoApiBaseUrl())
|
||||
.post('/graphql')
|
||||
.once()
|
||||
.reply(200, { data: { app: { __typename: 'AppQuery', byId: app } } });
|
||||
|
||||
const projectId = await getProjectIdAsync(
|
||||
sessionManager,
|
||||
{ name: 'app', slug: 'app', extra: { eas: { projectId: 'PROJECT_ID' } } },
|
||||
{ nonInteractive: true }
|
||||
);
|
||||
const { graphqlClient } = await new LoggedInContextField().getValueAsync({
|
||||
sessionManager,
|
||||
nonInteractive: true,
|
||||
} as ContextOptions);
|
||||
|
||||
expect(await AppQuery.byIdAsync(graphqlClient, projectId)).toMatchObject({
|
||||
fullName: '@owner/app',
|
||||
});
|
||||
expect(scope.isDone()).toBe(true);
|
||||
});
|
||||
});
|
||||
-144
@@ -1,144 +0,0 @@
|
||||
import { ExpoConfig } from '@expo/config';
|
||||
import { anything, instance, mock, when } from 'ts-mockito';
|
||||
|
||||
import { getPrivateExpoConfigAsync, getPublicExpoConfigAsync } from '../../../project/expoConfig';
|
||||
import SessionManager from '../../../user/SessionManager';
|
||||
import { ContextOptions } from '../ContextField';
|
||||
import {
|
||||
DynamicPrivateProjectConfigContextField,
|
||||
DynamicPublicProjectConfigContextField,
|
||||
} from '../DynamicProjectConfigContextField';
|
||||
import { OptionalPrivateProjectConfigContextField } from '../OptionalPrivateProjectConfigContextField';
|
||||
import { PrivateProjectConfigContextField } from '../PrivateProjectConfigContextField';
|
||||
import { findProjectDirAndVerifyProjectSetupAsync } from '../contextUtils/findProjectDirAndVerifyProjectSetupAsync';
|
||||
import { getProjectIdAsync } from '../contextUtils/getProjectIdAsync';
|
||||
import { loadServerSideEnvironmentVariablesAsync } from '../contextUtils/loadServerSideEnvironmentVariablesAsync';
|
||||
|
||||
jest.mock('../../../project/expoConfig');
|
||||
jest.mock('../contextUtils/findProjectDirAndVerifyProjectSetupAsync');
|
||||
jest.mock('../contextUtils/getProjectIdAsync', () => ({
|
||||
...jest.requireActual('../contextUtils/getProjectIdAsync'),
|
||||
getProjectIdAsync: jest.fn(),
|
||||
}));
|
||||
jest.mock('../contextUtils/loadServerSideEnvironmentVariablesAsync');
|
||||
jest.mock('../contextUtils/createGraphqlClient');
|
||||
|
||||
const projectId = 'test-project-id';
|
||||
const linkedExp = { name: 'a', slug: 'a', extra: { eas: { projectId } } } as ExpoConfig;
|
||||
const unlinkedExp = { name: 'a', slug: 'a' } as ExpoConfig;
|
||||
|
||||
type LoadConfigAsync = (
|
||||
options: ContextOptions
|
||||
) => Promise<{ exp: ExpoConfig; projectId: string } | undefined>;
|
||||
|
||||
const fields: [string, LoadConfigAsync, typeof getPrivateExpoConfigAsync][] = [
|
||||
[
|
||||
DynamicPublicProjectConfigContextField.name,
|
||||
async options =>
|
||||
await (
|
||||
await new DynamicPublicProjectConfigContextField().getValueAsync(options)
|
||||
)(),
|
||||
getPublicExpoConfigAsync,
|
||||
],
|
||||
[
|
||||
DynamicPrivateProjectConfigContextField.name,
|
||||
async options =>
|
||||
await (
|
||||
await new DynamicPrivateProjectConfigContextField().getValueAsync(options)
|
||||
)(),
|
||||
getPrivateExpoConfigAsync,
|
||||
],
|
||||
[
|
||||
PrivateProjectConfigContextField.name,
|
||||
async options => await new PrivateProjectConfigContextField().getValueAsync(options),
|
||||
getPrivateExpoConfigAsync,
|
||||
],
|
||||
[
|
||||
OptionalPrivateProjectConfigContextField.name,
|
||||
async options => await new OptionalPrivateProjectConfigContextField().getValueAsync(options),
|
||||
getPrivateExpoConfigAsync,
|
||||
],
|
||||
];
|
||||
|
||||
function createContextOptions(withServerSideEnvironment: string | null): ContextOptions {
|
||||
const sessionManager = mock<SessionManager>();
|
||||
when(sessionManager.ensureLoggedInAsync(anything())).thenResolve({
|
||||
authenticationInfo: { accessToken: null, sessionSecret: null },
|
||||
} as any);
|
||||
return {
|
||||
nonInteractive: true,
|
||||
sessionManager: instance(sessionManager),
|
||||
withServerSideEnvironment,
|
||||
} as ContextOptions;
|
||||
}
|
||||
|
||||
describe.each(fields)('%s', (_name, loadConfigAsync, getExpoConfigAsync) => {
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
jest.mocked(findProjectDirAndVerifyProjectSetupAsync).mockResolvedValue('/app');
|
||||
jest.mocked(getProjectIdAsync).mockResolvedValue(projectId);
|
||||
});
|
||||
|
||||
it('reads the app config once when the project ID is in it', async () => {
|
||||
jest.mocked(getExpoConfigAsync).mockResolvedValue(linkedExp);
|
||||
|
||||
const result = await loadConfigAsync(createContextOptions(null));
|
||||
|
||||
expect(result).toMatchObject({ exp: linkedExp, projectId });
|
||||
expect(getExpoConfigAsync).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('reads the app config again after the project ID is written to it', async () => {
|
||||
jest
|
||||
.mocked(getExpoConfigAsync)
|
||||
.mockResolvedValueOnce(unlinkedExp)
|
||||
.mockResolvedValueOnce(linkedExp);
|
||||
|
||||
const result = await loadConfigAsync(createContextOptions(null));
|
||||
|
||||
expect(result?.exp).toBe(linkedExp);
|
||||
expect(getExpoConfigAsync).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it('reads the app config again with server-side environment variables', async () => {
|
||||
jest.mocked(getExpoConfigAsync).mockResolvedValue(linkedExp);
|
||||
jest.mocked(loadServerSideEnvironmentVariablesAsync).mockResolvedValue({ B: '2' });
|
||||
|
||||
await loadConfigAsync(createContextOptions('production'));
|
||||
|
||||
expect(getExpoConfigAsync).toHaveBeenCalledTimes(2);
|
||||
expect(getExpoConfigAsync).toHaveBeenLastCalledWith('/app', { env: { B: '2' } });
|
||||
});
|
||||
|
||||
it('reads the app config again when the server-side environment is empty', async () => {
|
||||
jest.mocked(getExpoConfigAsync).mockResolvedValue(linkedExp);
|
||||
jest.mocked(loadServerSideEnvironmentVariablesAsync).mockResolvedValue({});
|
||||
|
||||
await loadConfigAsync(createContextOptions('production'));
|
||||
|
||||
expect(getExpoConfigAsync).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe(DynamicPrivateProjectConfigContextField, () => {
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
jest.mocked(findProjectDirAndVerifyProjectSetupAsync).mockResolvedValue('/app');
|
||||
jest.mocked(getProjectIdAsync).mockResolvedValue(projectId);
|
||||
});
|
||||
|
||||
it('merges server-side environment variables into the env passed in', async () => {
|
||||
jest.mocked(getPrivateExpoConfigAsync).mockResolvedValue(linkedExp);
|
||||
jest.mocked(loadServerSideEnvironmentVariablesAsync).mockResolvedValue({ B: '2' });
|
||||
|
||||
const getConfigAsync = await new DynamicPrivateProjectConfigContextField().getValueAsync(
|
||||
createContextOptions('production')
|
||||
);
|
||||
await getConfigAsync({ env: { A: '1' } });
|
||||
|
||||
expect(getPrivateExpoConfigAsync).toHaveBeenNthCalledWith(1, '/app', { env: { A: '1' } });
|
||||
expect(getPrivateExpoConfigAsync).toHaveBeenNthCalledWith(2, '/app', {
|
||||
env: { A: '1', B: '2' },
|
||||
});
|
||||
});
|
||||
});
|
||||
-7
@@ -10,7 +10,6 @@ import { fetchOrCreateProjectIDForWriteToConfigWithConfirmationAsync } from '../
|
||||
import { isExpoInstalled } from '../../../../project/projectUtils';
|
||||
import { promptAsync } from '../../../../prompts';
|
||||
import SessionManager from '../../../../user/SessionManager';
|
||||
import { ExpoGraphqlClient } from '../createGraphqlClient';
|
||||
import { findProjectRootAsync } from '../findProjectDirAndVerifyProjectSetupAsync';
|
||||
import { getProjectIdAsync } from '../getProjectIdAsync';
|
||||
|
||||
@@ -41,8 +40,6 @@ describe(getProjectIdAsync, () => {
|
||||
.mockReturnValue({ staticConfigPath: null, dynamicConfigPath: null });
|
||||
|
||||
const sessionManagerMock = mock<SessionManager>();
|
||||
|
||||
when(sessionManagerMock.getGraphqlClient(anything())).thenReturn({} as ExpoGraphqlClient);
|
||||
when(sessionManagerMock.ensureLoggedInAsync(anything())).thenResolve({
|
||||
actor: {
|
||||
__typename: 'User',
|
||||
@@ -189,7 +186,6 @@ describe(getProjectIdAsync, () => {
|
||||
|
||||
it('throws when the owner is not specified and is different than logged in user which is a robot', async () => {
|
||||
const sessionManagerMock = mock<SessionManager>();
|
||||
when(sessionManagerMock.getGraphqlClient(anything())).thenReturn({} as ExpoGraphqlClient);
|
||||
when(sessionManagerMock.ensureLoggedInAsync(anything())).thenResolve({
|
||||
actor: {
|
||||
__typename: 'Robot',
|
||||
@@ -414,7 +410,6 @@ describe(getProjectIdAsync, () => {
|
||||
|
||||
it('uses the only account without prompting when the user has a single account', async () => {
|
||||
const sessionManagerMock = mock<SessionManager>();
|
||||
when(sessionManagerMock.getGraphqlClient(anything())).thenReturn({} as ExpoGraphqlClient);
|
||||
when(sessionManagerMock.ensureLoggedInAsync(anything())).thenResolve({
|
||||
actor: {
|
||||
__typename: 'User',
|
||||
@@ -453,7 +448,6 @@ describe(getProjectIdAsync, () => {
|
||||
|
||||
it('throws for a robot user when no owner is set', async () => {
|
||||
const sessionManagerMock = mock<SessionManager>();
|
||||
when(sessionManagerMock.getGraphqlClient(anything())).thenReturn({} as ExpoGraphqlClient);
|
||||
when(sessionManagerMock.ensureLoggedInAsync(anything())).thenResolve({
|
||||
actor: {
|
||||
__typename: 'Robot',
|
||||
@@ -515,7 +509,6 @@ describe(getProjectIdAsync, () => {
|
||||
|
||||
it('defers to the project fetch in non-interactive mode when the account is already known', async () => {
|
||||
const sessionManagerMock = mock<SessionManager>();
|
||||
when(sessionManagerMock.getGraphqlClient(anything())).thenReturn({} as ExpoGraphqlClient);
|
||||
when(sessionManagerMock.ensureLoggedInAsync(anything())).thenResolve({
|
||||
actor: {
|
||||
__typename: 'User',
|
||||
|
||||
@@ -3,7 +3,7 @@ import { Env } from '@expo/eas-build-job';
|
||||
import chalk from 'chalk';
|
||||
import semver from 'semver';
|
||||
|
||||
import { ExpoGraphqlClient } from './createGraphqlClient';
|
||||
import { ExpoGraphqlClient, createGraphqlClient } from './createGraphqlClient';
|
||||
import { findProjectRootAsync } from './findProjectDirAndVerifyProjectSetupAsync';
|
||||
import { AppQuery } from '../../../graphql/queries/AppQuery';
|
||||
import Log, { learnMore } from '../../../log';
|
||||
@@ -92,16 +92,12 @@ export async function getProjectIdAsync(
|
||||
const { actor, authenticationInfo } = await sessionManager.ensureLoggedInAsync({
|
||||
nonInteractive: options.nonInteractive,
|
||||
});
|
||||
const graphqlClient = sessionManager.getGraphqlClient(authenticationInfo);
|
||||
const graphqlClient = createGraphqlClient(authenticationInfo);
|
||||
|
||||
const projectId = await validateOrSetProjectIdAsync({ exp, graphqlClient, actor, options });
|
||||
return projectId;
|
||||
}
|
||||
|
||||
export function isProjectIdInAppConfig(exp: Pick<ExpoConfig, 'extra'>, projectId: string): boolean {
|
||||
return exp.extra?.eas?.projectId === projectId;
|
||||
}
|
||||
|
||||
export async function validateOrSetProjectIdAsync({
|
||||
exp,
|
||||
graphqlClient,
|
||||
|
||||
@@ -109,7 +109,7 @@ export default class BuildDev extends EasCommand {
|
||||
|
||||
const workflow = await resolveWorkflowAsync(projectDir, platform, vcsClient);
|
||||
|
||||
const evaluatedConfig = await evaluateConfigWithEnvVarsAsync({
|
||||
const { env } = await evaluateConfigWithEnvVarsAsync({
|
||||
buildProfile: buildProfile.profile,
|
||||
buildProfileName: buildProfile.profileName,
|
||||
graphqlClient,
|
||||
@@ -118,7 +118,7 @@ export default class BuildDev extends EasCommand {
|
||||
});
|
||||
|
||||
const fingerprint = await createFingerprintAsync(projectDir, {
|
||||
env: evaluatedConfig.env,
|
||||
env,
|
||||
workflow,
|
||||
platforms: [platform],
|
||||
});
|
||||
@@ -197,7 +197,7 @@ export default class BuildDev extends EasCommand {
|
||||
actor,
|
||||
getDynamicPrivateProjectConfigAsync,
|
||||
downloadSimBuildAutoConfirm: true,
|
||||
evaluatedConfig,
|
||||
envOverride: env,
|
||||
});
|
||||
if (!flags['skip-bundler']) {
|
||||
await this.startDevServerAsync({ projectDir, platform });
|
||||
|
||||
+134
-137
@@ -183,26 +183,12 @@ export type Account = {
|
||||
/** Convex team connections for this account */
|
||||
convexTeamConnections: Array<ConvexTeamConnection>;
|
||||
createdAt: Scalars['DateTime']['output'];
|
||||
/**
|
||||
* Whether the account turned on credentials protection. When protection is on, reading,
|
||||
* creating, changing, or deleting build, submission, and push credentials needs the
|
||||
* PUBLISH_PROTECTED permission, in every project of the account.
|
||||
*/
|
||||
credentialsProtectionEnabled: Scalars['Boolean']['output'];
|
||||
/** Simulator sessions for all apps associated with this account, sorted newest first. */
|
||||
deviceRunSessionsPaginated: AccountDeviceRunSessionsConnection;
|
||||
displayName?: Maybe<Scalars['String']['output']>;
|
||||
/** Environment secrets for an account */
|
||||
environmentSecrets: Array<EnvironmentSecret>;
|
||||
environmentVariableEnvironments: Array<Scalars['EnvironmentVariableEnvironment']['output']>;
|
||||
/**
|
||||
* Whether the account turned on environment variable protection. When protection is on,
|
||||
* reading, using, creating, changing, or deleting an environment variable whose environments are
|
||||
* exclusively production needs the PUBLISH_PROTECTED permission, whatever its visibility, in
|
||||
* every project of the account. Expo can also turn protection on for an account; this field
|
||||
* does not show that.
|
||||
*/
|
||||
environmentVariableProtectionEnabled: Scalars['Boolean']['output'];
|
||||
/** Environment variables for an account */
|
||||
environmentVariables: Array<EnvironmentVariable>;
|
||||
/** Environment variables for an account with decrypted secret values */
|
||||
@@ -782,23 +768,8 @@ export type AccountMutation = {
|
||||
requestRefund?: Maybe<Scalars['Boolean']['output']>;
|
||||
/** Revoke specified Permissions for Actor. Actor must already have at least one permission on the account. */
|
||||
revokeActorPermissions: Account;
|
||||
/**
|
||||
* Set whether credentials protection is on for this account. When it is on, reading, creating,
|
||||
* changing, or deleting build, submission, and push credentials needs the PUBLISH_PROTECTED
|
||||
* permission on the account, in every project of the account. Requires the ADMIN permission on
|
||||
* the account.
|
||||
*/
|
||||
setCredentialsProtectionEnabled: Account;
|
||||
/** Set the display name for the account. Pass null to clear it and fall back to the account identifier. */
|
||||
setDisplayName: Account;
|
||||
/**
|
||||
* Set whether environment variable protection is on for this account. When it is on, reading,
|
||||
* using, creating, changing, or deleting an environment variable whose environments are
|
||||
* exclusively production needs the PUBLISH_PROTECTED permission on the account, whatever its
|
||||
* visibility, in every project of the account. Requires the ADMIN permission on the account.
|
||||
* Turning protection off also requires a sudo session.
|
||||
*/
|
||||
setEnvironmentVariableProtectionEnabled: Account;
|
||||
/** Set whether EAS Observe ingestion is paused for this account. */
|
||||
setObserveIngestionPaused: Account;
|
||||
/** Require authorization to send push notifications for experiences owned by this account */
|
||||
@@ -871,24 +842,12 @@ export type AccountMutation_RevokeActorPermissionsArgs = {
|
||||
};
|
||||
|
||||
|
||||
export type AccountMutation_SetCredentialsProtectionEnabledArgs = {
|
||||
accountID: Scalars['ID']['input'];
|
||||
credentialsProtectionEnabled: Scalars['Boolean']['input'];
|
||||
};
|
||||
|
||||
|
||||
export type AccountMutation_SetDisplayNameArgs = {
|
||||
accountID: Scalars['ID']['input'];
|
||||
displayName?: InputMaybe<Scalars['String']['input']>;
|
||||
};
|
||||
|
||||
|
||||
export type AccountMutation_SetEnvironmentVariableProtectionEnabledArgs = {
|
||||
accountID: Scalars['ID']['input'];
|
||||
environmentVariableProtectionEnabled: Scalars['Boolean']['input'];
|
||||
};
|
||||
|
||||
|
||||
export type AccountMutation_SetObserveIngestionPausedArgs = {
|
||||
accountID: Scalars['ID']['input'];
|
||||
paused: Scalars['Boolean']['input'];
|
||||
@@ -1203,11 +1162,6 @@ export type AgentDeviceRunSessionRemoteConfig = {
|
||||
webPreviewUrl?: Maybe<Scalars['String']['output']>;
|
||||
};
|
||||
|
||||
export enum AgentKind {
|
||||
ClaudeCode = 'CLAUDE_CODE',
|
||||
Codex = 'CODEX'
|
||||
}
|
||||
|
||||
export enum AgentProvider {
|
||||
Anthropic = 'ANTHROPIC',
|
||||
Openai = 'OPENAI'
|
||||
@@ -1282,44 +1236,6 @@ export enum AgentProviderConnectionStatus {
|
||||
Valid = 'VALID'
|
||||
}
|
||||
|
||||
export type AgentRun = {
|
||||
__typename?: 'AgentRun';
|
||||
agentKind: AgentKind;
|
||||
createdAt: Scalars['DateTime']['output'];
|
||||
id: Scalars['ID']['output'];
|
||||
maxDurationSeconds: Scalars['Int']['output'];
|
||||
prompt?: Maybe<Scalars['String']['output']>;
|
||||
turtleJobRun: JobRun;
|
||||
updatedAt: Scalars['DateTime']['output'];
|
||||
};
|
||||
|
||||
export type AgentRunConnection = {
|
||||
__typename?: 'AgentRunConnection';
|
||||
edges: Array<AgentRunEdge>;
|
||||
pageInfo: PageInfo;
|
||||
};
|
||||
|
||||
export type AgentRunEdge = {
|
||||
__typename?: 'AgentRunEdge';
|
||||
cursor: Scalars['String']['output'];
|
||||
node: AgentRun;
|
||||
};
|
||||
|
||||
export type AgentRunFilterInput = {
|
||||
agentKinds?: InputMaybe<Array<AgentKind>>;
|
||||
statuses?: InputMaybe<Array<JobRunStatus>>;
|
||||
};
|
||||
|
||||
export type AgentRunQuery = {
|
||||
__typename?: 'AgentRunQuery';
|
||||
byId: AgentRun;
|
||||
};
|
||||
|
||||
|
||||
export type AgentRunQuery_ByIdArgs = {
|
||||
agentRunId: Scalars['ID']['input'];
|
||||
};
|
||||
|
||||
export type AndroidAppBuildCredentials = {
|
||||
__typename?: 'AndroidAppBuildCredentials';
|
||||
androidKeystore?: Maybe<AndroidKeystore>;
|
||||
@@ -1680,8 +1596,6 @@ export type App = Project & {
|
||||
__typename?: 'App';
|
||||
/** Coalesced project activity for an app */
|
||||
activityTimelineProjectActivities: Array<ActivityTimelineProjectActivity>;
|
||||
/** Agent runs for this project, newest first. Empty filters include all runs. */
|
||||
agentRunsPaginated: AgentRunConnection;
|
||||
/** Android app credentials for the project */
|
||||
androidAppCredentials: Array<AndroidAppCredentials>;
|
||||
appStoreConnectApp?: Maybe<AppStoreConnectApp>;
|
||||
@@ -1845,16 +1759,6 @@ export type App_ActivityTimelineProjectActivitiesArgs = {
|
||||
};
|
||||
|
||||
|
||||
/** Represents an Exponent App (or Experience in legacy terms) */
|
||||
export type App_AgentRunsPaginatedArgs = {
|
||||
after?: InputMaybe<Scalars['String']['input']>;
|
||||
before?: InputMaybe<Scalars['String']['input']>;
|
||||
filter?: InputMaybe<AgentRunFilterInput>;
|
||||
first?: InputMaybe<Scalars['Int']['input']>;
|
||||
last?: InputMaybe<Scalars['Int']['input']>;
|
||||
};
|
||||
|
||||
|
||||
/** Represents an Exponent App (or Experience in legacy terms) */
|
||||
export type App_AndroidAppCredentialsArgs = {
|
||||
filter?: InputMaybe<AndroidAppCredentialsFilter>;
|
||||
@@ -2487,6 +2391,11 @@ export type AppObserve = {
|
||||
* @deprecated Use overview.stability instead.
|
||||
*/
|
||||
overviewStability: AppObserveOverviewStability;
|
||||
/**
|
||||
* Per-version, per-platform metric summaries for the Overview version-comparison matrix.
|
||||
* @deprecated Use overview.versionComparison instead.
|
||||
*/
|
||||
overviewVersionComparison: AppObserveOverviewVersionComparison;
|
||||
/** One session's metrics and logs for the timeline (ENG-26160). Unknown session ids yield empty lists. */
|
||||
session: AppObserveSession;
|
||||
/** @deprecated Use metrics.timeSeries instead. */
|
||||
@@ -2501,6 +2410,11 @@ export type AppObserve = {
|
||||
* universe used by `appVersions` headline counts.
|
||||
*/
|
||||
uniqueActiveUserCount: Scalars['Int']['output'];
|
||||
/**
|
||||
* Update download activity (the Recent updates list). Not available on the free tier or legacy plans.
|
||||
* @deprecated Use easUpdates.list instead.
|
||||
*/
|
||||
updates: AppObserveUpdatesConnection;
|
||||
/** Namespaced successor API (ENG-26160). Prefer over the flat `customEventNames`/`customEventList` fields. */
|
||||
userEvents: AppObserveUserEvents;
|
||||
};
|
||||
@@ -2589,6 +2503,11 @@ export type AppObserve_OverviewStabilityArgs = {
|
||||
};
|
||||
|
||||
|
||||
export type AppObserve_OverviewVersionComparisonArgs = {
|
||||
input: AppObserveOverviewVersionComparisonInput;
|
||||
};
|
||||
|
||||
|
||||
export type AppObserve_SessionArgs = {
|
||||
id: Scalars['ID']['input'];
|
||||
};
|
||||
@@ -2603,6 +2522,11 @@ export type AppObserve_UniqueActiveUserCountArgs = {
|
||||
input: AppObserveReleasesInput;
|
||||
};
|
||||
|
||||
|
||||
export type AppObserve_UpdatesArgs = {
|
||||
input: AppObserveUpdatesInput;
|
||||
};
|
||||
|
||||
export type AppObserveAppBuildEmbeddedSummary = {
|
||||
__typename?: 'AppObserveAppBuildEmbeddedSummary';
|
||||
eventCount: Scalars['Int']['output'];
|
||||
@@ -4043,6 +3967,31 @@ export type AppObserveUpdatesConnection = {
|
||||
totalDownloads: Scalars['Int']['output'];
|
||||
};
|
||||
|
||||
export type AppObserveUpdatesInput = {
|
||||
after?: InputMaybe<Scalars['String']['input']>;
|
||||
appBuildNumber?: InputMaybe<Scalars['String']['input']>;
|
||||
appUpdateId?: InputMaybe<Scalars['String']['input']>;
|
||||
appVersion?: InputMaybe<Scalars['String']['input']>;
|
||||
endTime: Scalars['DateTime']['input'];
|
||||
environment?: InputMaybe<Scalars['String']['input']>;
|
||||
first?: InputMaybe<Scalars['Int']['input']>;
|
||||
orderBy?: InputMaybe<AppObserveUpdatesOrderBy>;
|
||||
platform?: InputMaybe<AppObservePlatform>;
|
||||
/** Filter to these platforms. Cannot be set together with platform. */
|
||||
platforms?: InputMaybe<Array<AppObservePlatform>>;
|
||||
startTime: Scalars['DateTime']['input'];
|
||||
};
|
||||
|
||||
export type AppObserveUpdatesOrderBy = {
|
||||
direction: AppObserveUpdatesOrderByDirection;
|
||||
field: AppObserveUpdatesOrderByField;
|
||||
};
|
||||
|
||||
export enum AppObserveUpdatesOrderByDirection {
|
||||
Asc = 'ASC',
|
||||
Desc = 'DESC'
|
||||
}
|
||||
|
||||
export enum AppObserveUpdatesOrderByField {
|
||||
DownloadCount = 'DOWNLOAD_COUNT',
|
||||
FirstSeenAt = 'FIRST_SEEN_AT',
|
||||
@@ -5426,7 +5375,6 @@ export enum AuthProviderIdentifier {
|
||||
MsEntraId = 'MS_ENTRA_ID',
|
||||
Okta = 'OKTA',
|
||||
OneLogin = 'ONE_LOGIN',
|
||||
Rippling = 'RIPPLING',
|
||||
StubIdp = 'STUB_IDP'
|
||||
}
|
||||
|
||||
@@ -5575,7 +5523,6 @@ export type Build = ActivityTimelineProjectActivity & BuildOrBuildJob & {
|
||||
embeddedUpdate?: Maybe<EmbeddedUpdate>;
|
||||
enqueuedAt?: Maybe<Scalars['DateTime']['output']>;
|
||||
error?: Maybe<BuildError>;
|
||||
/** @deprecated Build wait estimates are no longer available. This field returns null. */
|
||||
estimatedWaitTimeLeftSeconds?: Maybe<Scalars['Int']['output']>;
|
||||
expirationDate?: Maybe<Scalars['DateTime']['output']>;
|
||||
fingerprint?: Maybe<Fingerprint>;
|
||||
@@ -5583,7 +5530,7 @@ export type Build = ActivityTimelineProjectActivity & BuildOrBuildJob & {
|
||||
gitCommitMessage?: Maybe<Scalars['String']['output']>;
|
||||
gitRef?: Maybe<Scalars['String']['output']>;
|
||||
id: Scalars['ID']['output'];
|
||||
/** @deprecated Build queue position is no longer available. This field returns null. */
|
||||
/** Queue position is 1-indexed */
|
||||
initialQueuePosition?: Maybe<Scalars['Int']['output']>;
|
||||
initiatingActor?: Maybe<Actor>;
|
||||
iosEnterpriseProvisioning?: Maybe<BuildIosEnterpriseProvisioning>;
|
||||
@@ -5606,7 +5553,7 @@ export type Build = ActivityTimelineProjectActivity & BuildOrBuildJob & {
|
||||
projectMetadataFileUrl?: Maybe<Scalars['String']['output']>;
|
||||
projectRootDirectory?: Maybe<Scalars['String']['output']>;
|
||||
provisioningStartedAt?: Maybe<Scalars['DateTime']['output']>;
|
||||
/** @deprecated Build queue position is no longer available. This field returns null. */
|
||||
/** Queue position is 1-indexed */
|
||||
queuePosition?: Maybe<Scalars['Int']['output']>;
|
||||
reactNativeVersion?: Maybe<Scalars['String']['output']>;
|
||||
releaseChannel?: Maybe<Scalars['String']['output']>;
|
||||
@@ -5941,11 +5888,6 @@ export type BuildOrBuildJob = {
|
||||
};
|
||||
|
||||
export type BuildParamsInput = {
|
||||
/**
|
||||
* The role of the EXPO_TOKEN that the build gets. The caller must have at least this role on the
|
||||
* account, or the mutation fails. Without it, the token has the default permissions.
|
||||
*/
|
||||
expoTokenRole?: InputMaybe<JobExpoTokenRole>;
|
||||
reactNativeVersion?: InputMaybe<Scalars['String']['input']>;
|
||||
resourceClass: BuildResourceClass;
|
||||
sdkVersion?: InputMaybe<Scalars['String']['input']>;
|
||||
@@ -6415,6 +6357,24 @@ export type CreateConvexTeamConnectionInput = {
|
||||
deploymentRegion: Scalars['String']['input'];
|
||||
};
|
||||
|
||||
export type CreateDeviceRunSessionArtifactUploadSessionInput = {
|
||||
filename: Scalars['String']['input'];
|
||||
kind?: InputMaybe<Scalars['String']['input']>;
|
||||
metadata?: InputMaybe<Scalars['JSONObject']['input']>;
|
||||
name: Scalars['String']['input'];
|
||||
size: Scalars['ByteSize']['input'];
|
||||
};
|
||||
|
||||
export type CreateDeviceRunSessionArtifactUploadSessionResult = {
|
||||
__typename?: 'CreateDeviceRunSessionArtifactUploadSessionResult';
|
||||
uploadSession: DeviceRunSessionArtifactUploadSession;
|
||||
};
|
||||
|
||||
export type CreateDeviceRunSessionEventLogUploadSessionResult = {
|
||||
__typename?: 'CreateDeviceRunSessionEventLogUploadSessionResult';
|
||||
uploadSession: DeviceRunSessionEventLogUploadSession;
|
||||
};
|
||||
|
||||
export type CreateDeviceRunSessionInput = {
|
||||
/**
|
||||
* The coding agent sending this request, recorded for analytics only and never used for
|
||||
@@ -6447,14 +6407,6 @@ export type CreateDeviceRunSessionInput = {
|
||||
* Mutually exclusive with buildFingerprint, applicationArchiveUrl, and expoGo.
|
||||
*/
|
||||
buildId?: InputMaybe<Scalars['ID']['input']>;
|
||||
/**
|
||||
* Origins of pages that may call the session's preview host API cross-origin, for example to
|
||||
* render the device with @expo/hub-client. Framing the preview does not need this. Same value
|
||||
* rules as frameAncestors. At most 10. The page still needs the preview token from remoteConfig.
|
||||
* If omitted, only expo.dev may call the preview API cross-origin. Available on Production and
|
||||
* Enterprise plans.
|
||||
*/
|
||||
corsOrigins?: InputMaybe<Array<Scalars['String']['input']>>;
|
||||
/**
|
||||
* Where the virtual device's proxied network traffic exits to the internet. If
|
||||
* omitted, all traffic exits from EAS infrastructure.
|
||||
@@ -6472,7 +6424,6 @@ export type CreateDeviceRunSessionInput = {
|
||||
* as https://*.example.com; plain http is accepted for localhost only. At most 10. The
|
||||
* embedding page still needs the preview token from remoteConfig, and whoever embeds the
|
||||
* preview can also control the device. If omitted, only expo.dev may embed the preview.
|
||||
* Available on Production and Enterprise plans.
|
||||
*/
|
||||
frameAncestors?: InputMaybe<Array<Scalars['String']['input']>>;
|
||||
/** iOS Simulator options. Only supported when platform is IOS. */
|
||||
@@ -6484,7 +6435,7 @@ export type CreateDeviceRunSessionInput = {
|
||||
launchArgs?: InputMaybe<Array<Scalars['String']['input']>>;
|
||||
/**
|
||||
* Stop the session automatically after this many minutes without observed
|
||||
* session activity. Must be positive and no greater than the session's maximum
|
||||
* session activity. Must be positive and smaller than the session's maximum
|
||||
* duration (maxRunTimeMinutes when set). Only supported for
|
||||
* agent-device, argent, and Appium sessions. If omitted, the session has no idle
|
||||
* timeout.
|
||||
@@ -6613,6 +6564,13 @@ export type CreateGitHubRepositorySettingsInput = {
|
||||
};
|
||||
|
||||
export type CreateIosDeviceRunSessionInput = {
|
||||
/**
|
||||
* Origins of pages that may call the session's preview host API cross-origin, for example to
|
||||
* render the device with @expo/hub-client. Framing the preview does not need this. Same value
|
||||
* rules as frameAncestors. At most 10. The page still needs the preview token from remoteConfig.
|
||||
* If omitted, only expo.dev may call the preview API cross-origin.
|
||||
*/
|
||||
corsOrigins?: InputMaybe<Array<Scalars['String']['input']>>;
|
||||
/** Simulator device name or UDID to use for the session (e.g. "iPhone 16 Pro"). */
|
||||
deviceIdentifier?: InputMaybe<Scalars['String']['input']>;
|
||||
};
|
||||
@@ -6653,7 +6611,6 @@ export type CreateSandboxInput = {
|
||||
agentRunId?: InputMaybe<Scalars['ID']['input']>;
|
||||
appId: Scalars['ID']['input'];
|
||||
image?: InputMaybe<Scalars['String']['input']>;
|
||||
maxDurationSeconds?: InputMaybe<Scalars['Int']['input']>;
|
||||
operatingSystem: SandboxOperatingSystem;
|
||||
projectArchive?: InputMaybe<SandboxProjectArchiveInput>;
|
||||
resourceClass: SandboxResourceClass;
|
||||
@@ -7177,6 +7134,12 @@ export type DeviceRunSessionArtifactQuery_ByIdArgs = {
|
||||
deviceRunSessionArtifactId: Scalars['ID']['input'];
|
||||
};
|
||||
|
||||
export type DeviceRunSessionArtifactUploadSession = {
|
||||
__typename?: 'DeviceRunSessionArtifactUploadSession';
|
||||
headers: Scalars['JSONObject']['output'];
|
||||
url: Scalars['String']['output'];
|
||||
};
|
||||
|
||||
/**
|
||||
* Where the virtual device's proxied network traffic exits to the internet. When
|
||||
* unset, all traffic exits from EAS infrastructure.
|
||||
@@ -7193,6 +7156,12 @@ export enum DeviceRunSessionEgress {
|
||||
Local = 'LOCAL'
|
||||
}
|
||||
|
||||
export type DeviceRunSessionEventLogUploadSession = {
|
||||
__typename?: 'DeviceRunSessionEventLogUploadSession';
|
||||
headers: Scalars['JSONObject']['output'];
|
||||
url: Scalars['String']['output'];
|
||||
};
|
||||
|
||||
export type DeviceRunSessionFilterInput = {
|
||||
/** Case-insensitive match anywhere in the session name. */
|
||||
name?: InputMaybe<Scalars['String']['input']>;
|
||||
@@ -7205,8 +7174,20 @@ export type DeviceRunSessionFilterInput = {
|
||||
|
||||
export type DeviceRunSessionMutation = {
|
||||
__typename?: 'DeviceRunSessionMutation';
|
||||
/** Create an upload session for an artifact while the backing job run is not in a final state */
|
||||
createArtifactUploadSession: CreateDeviceRunSessionArtifactUploadSessionResult;
|
||||
/** Create a device run session */
|
||||
createDeviceRunSession: DeviceRunSession;
|
||||
/**
|
||||
* Create or reuse a standard artifact and a two-hour upload URL for repeatedly
|
||||
* overwriting its structured event log while the backing job run is running.
|
||||
*/
|
||||
createEventLogUploadSession: CreateDeviceRunSessionEventLogUploadSessionResult;
|
||||
/**
|
||||
* Create or renew a two-hour upload URL for overwriting the session preview.
|
||||
* Reuses one WebP artifact while the backing job run is active.
|
||||
*/
|
||||
createPreviewUploadSession: CreateDeviceRunSessionArtifactUploadSessionResult;
|
||||
/**
|
||||
* Permanently delete a finished session and its artifacts. The backing job,
|
||||
* if present, must also have finished. Returns the deleted session ID.
|
||||
@@ -7218,6 +7199,8 @@ export type DeviceRunSessionMutation = {
|
||||
* ERRORED).
|
||||
*/
|
||||
ensureDeviceRunSessionStopped: DeviceRunSession;
|
||||
/** Mark a device run session as started and persist remote connection details */
|
||||
startDeviceRunSession: DeviceRunSession;
|
||||
/**
|
||||
* Update a device run session name and tags, including on sessions
|
||||
* that have already finished.
|
||||
@@ -7226,11 +7209,27 @@ export type DeviceRunSessionMutation = {
|
||||
};
|
||||
|
||||
|
||||
export type DeviceRunSessionMutation_CreateArtifactUploadSessionArgs = {
|
||||
deviceRunSessionId: Scalars['ID']['input'];
|
||||
input: CreateDeviceRunSessionArtifactUploadSessionInput;
|
||||
};
|
||||
|
||||
|
||||
export type DeviceRunSessionMutation_CreateDeviceRunSessionArgs = {
|
||||
deviceRunSessionInput: CreateDeviceRunSessionInput;
|
||||
};
|
||||
|
||||
|
||||
export type DeviceRunSessionMutation_CreateEventLogUploadSessionArgs = {
|
||||
deviceRunSessionId: Scalars['ID']['input'];
|
||||
};
|
||||
|
||||
|
||||
export type DeviceRunSessionMutation_CreatePreviewUploadSessionArgs = {
|
||||
deviceRunSessionId: Scalars['ID']['input'];
|
||||
};
|
||||
|
||||
|
||||
export type DeviceRunSessionMutation_DeleteDeviceRunSessionArgs = {
|
||||
deviceRunSessionId: Scalars['ID']['input'];
|
||||
};
|
||||
@@ -7241,6 +7240,12 @@ export type DeviceRunSessionMutation_EnsureDeviceRunSessionStoppedArgs = {
|
||||
};
|
||||
|
||||
|
||||
export type DeviceRunSessionMutation_StartDeviceRunSessionArgs = {
|
||||
deviceRunSessionId: Scalars['ID']['input'];
|
||||
remoteConfig: Scalars['JSONObject']['input'];
|
||||
};
|
||||
|
||||
|
||||
export type DeviceRunSessionMutation_UpdateDeviceRunSessionArgs = {
|
||||
deviceRunSessionId: Scalars['ID']['input'];
|
||||
input: UpdateDeviceRunSessionInput;
|
||||
@@ -7523,9 +7528,7 @@ export enum EntityTypeName {
|
||||
ConvexProjectEntity = 'ConvexProjectEntity',
|
||||
ConvexTeamConnectionEntity = 'ConvexTeamConnectionEntity',
|
||||
CustomerEntity = 'CustomerEntity',
|
||||
/** @deprecated This entity type was deleted. Existing audit logs can still reference it. */
|
||||
EchoProjectEntity = 'EchoProjectEntity',
|
||||
/** @deprecated This entity type was deleted. Existing audit logs can still reference it. */
|
||||
EchoVersionEntity = 'EchoVersionEntity',
|
||||
EnvironmentVariableEntity = 'EnvironmentVariableEntity',
|
||||
GoogleServiceAccountKeyEntity = 'GoogleServiceAccountKeyEntity',
|
||||
@@ -8520,6 +8523,11 @@ export type GitHubUserMutation = {
|
||||
__typename?: 'GitHubUserMutation';
|
||||
/** Delete a GitHub User by ID */
|
||||
deleteGitHubUser: DeleteGitHubUserResult;
|
||||
/**
|
||||
* Generate a GitHub User Access Token
|
||||
* @deprecated This mutation is scheduled for removal.
|
||||
*/
|
||||
generateGitHubUserAccessToken?: Maybe<Scalars['String']['output']>;
|
||||
};
|
||||
|
||||
|
||||
@@ -8527,6 +8535,11 @@ export type GitHubUserMutation_DeleteGitHubUserArgs = {
|
||||
id: Scalars['ID']['input'];
|
||||
};
|
||||
|
||||
|
||||
export type GitHubUserMutation_GenerateGitHubUserAccessTokenArgs = {
|
||||
githubAppRegistrationId?: InputMaybe<Scalars['ID']['input']>;
|
||||
};
|
||||
|
||||
export type GoogleServiceAccountKey = {
|
||||
__typename?: 'GoogleServiceAccountKey';
|
||||
account: Account;
|
||||
@@ -8955,17 +8968,9 @@ export type IosSubmissionConfigInput = {
|
||||
isVerboseFastlaneEnabled?: InputMaybe<Scalars['Boolean']['input']>;
|
||||
};
|
||||
|
||||
/** A role that a build can request for its EXPO_TOKEN. */
|
||||
export enum JobExpoTokenRole {
|
||||
Developer = 'DEVELOPER',
|
||||
ReleaseManager = 'RELEASE_MANAGER',
|
||||
Runner = 'RUNNER'
|
||||
}
|
||||
|
||||
/** Represents a Turtle Job Run */
|
||||
export type JobRun = {
|
||||
__typename?: 'JobRun';
|
||||
agentRun?: Maybe<AgentRun>;
|
||||
app: App;
|
||||
artifacts: Array<WorkflowArtifact>;
|
||||
createdAt: Scalars['DateTime']['output'];
|
||||
@@ -9997,7 +10002,7 @@ export type RootMutation = {
|
||||
deployments: DeploymentsMutation;
|
||||
/** Mutations that assign or modify DevDomainNames for apps */
|
||||
devDomainName: AppDevDomainNameMutation;
|
||||
/** Mutations that create, stop, update, and delete device run sessions */
|
||||
/** Mutations that create, start, stop, and update device run sessions */
|
||||
deviceRunSession: DeviceRunSessionMutation;
|
||||
/** Mutations for Discord users */
|
||||
discordUser: DiscordUserMutation;
|
||||
@@ -10117,7 +10122,6 @@ export type RootQuery = {
|
||||
account: AccountQuery;
|
||||
/** Top-level query object for querying AccountSSOConfigurationPublicData */
|
||||
accountSSOConfigurationPublicData: AccountSsoConfigurationPublicDataQuery;
|
||||
agentRuns: AgentRunQuery;
|
||||
app: AppQuery;
|
||||
/**
|
||||
* Look up app by app id
|
||||
@@ -10441,7 +10445,6 @@ export type Sandbox = {
|
||||
finishedAt?: Maybe<Scalars['DateTime']['output']>;
|
||||
id: Scalars['ID']['output'];
|
||||
lastUsedAt: Scalars['DateTime']['output'];
|
||||
maxDurationSeconds?: Maybe<Scalars['Int']['output']>;
|
||||
startedAt?: Maybe<Scalars['DateTime']['output']>;
|
||||
status: SandboxStatus;
|
||||
turtleJobRun: JobRun;
|
||||
@@ -10721,6 +10724,8 @@ export type ServeSimRunSessionRemoteConfig = {
|
||||
/** Session token gating the preview. Null when the preview runs ungated. */
|
||||
previewToken?: Maybe<Scalars['String']['output']>;
|
||||
previewUrl: Scalars['String']['output'];
|
||||
/** @deprecated Use previewUrl instead. */
|
||||
streamUrl?: Maybe<Scalars['String']['output']>;
|
||||
};
|
||||
|
||||
export type SetSupabaseConnectionOrganizationInput = {
|
||||
@@ -12089,15 +12094,11 @@ export type UsageBudget = {
|
||||
export type UsageBudgetLimits = {
|
||||
__typename?: 'UsageBudgetLimits';
|
||||
builds?: Maybe<Scalars['Int']['output']>;
|
||||
concurrentSandboxes?: Maybe<Scalars['Int']['output']>;
|
||||
sandboxMinutes?: Maybe<Scalars['Int']['output']>;
|
||||
simulatorMinutes?: Maybe<Scalars['Int']['output']>;
|
||||
};
|
||||
|
||||
export type UsageBudgetLimitsInput = {
|
||||
builds?: InputMaybe<Scalars['Int']['input']>;
|
||||
concurrentSandboxes?: InputMaybe<Scalars['Int']['input']>;
|
||||
sandboxMinutes?: InputMaybe<Scalars['Int']['input']>;
|
||||
simulatorMinutes?: InputMaybe<Scalars['Int']['input']>;
|
||||
};
|
||||
|
||||
@@ -12125,10 +12126,6 @@ export type UsageBudgetRemaining = {
|
||||
__typename?: 'UsageBudgetRemaining';
|
||||
/** Available builds, or null when there is no build limit. */
|
||||
builds?: Maybe<Scalars['Int']['output']>;
|
||||
/** Available sandbox slots, including starting and running sandboxes, or null when unlimited. */
|
||||
concurrentSandboxes?: Maybe<Scalars['Int']['output']>;
|
||||
/** Available minutes after completed usage and active reservations, or null when unlimited. */
|
||||
sandboxMinutes?: Maybe<Scalars['Float']['output']>;
|
||||
/** Available minutes after completed usage and active reservations, or null when unlimited. */
|
||||
simulatorMinutes?: Maybe<Scalars['Float']['output']>;
|
||||
};
|
||||
|
||||
@@ -9,10 +9,7 @@ import { fetchUserAsync } from './fetchUser';
|
||||
import { ApiV2Error } from '../ApiV2Error';
|
||||
import { AnalyticsWithOrchestration } from '../analytics/AnalyticsManager';
|
||||
import { ApiV2Client } from '../api';
|
||||
import {
|
||||
ExpoGraphqlClient,
|
||||
createGraphqlClient,
|
||||
} from '../commandUtils/context/contextUtils/createGraphqlClient';
|
||||
import { createGraphqlClient } from '../commandUtils/context/contextUtils/createGraphqlClient';
|
||||
import { CurrentUserQuery } from '../graphql/generated';
|
||||
import { UserQuery } from '../graphql/queries/UserQuery';
|
||||
import Log, { learnMore } from '../log';
|
||||
@@ -46,7 +43,6 @@ type Actor = NonNullable<CurrentUserQuery['meActor']>;
|
||||
|
||||
export default class SessionManager {
|
||||
private currentActor: Actor | undefined;
|
||||
private graphqlClient: { credential: string | null; client: ExpoGraphqlClient } | undefined;
|
||||
|
||||
constructor(private readonly analytics: AnalyticsWithOrchestration) {}
|
||||
|
||||
@@ -91,21 +87,6 @@ export default class SessionManager {
|
||||
await this.setSessionAsync(undefined);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns one GraphQL client per credential, so context fields of one command share
|
||||
* the urql cache. For example, the project ID check and the command both query the app.
|
||||
*/
|
||||
public getGraphqlClient(authenticationInfo: {
|
||||
accessToken: string | null;
|
||||
sessionSecret: string | null;
|
||||
}): ExpoGraphqlClient {
|
||||
const credential = authenticationInfo.accessToken ?? authenticationInfo.sessionSecret;
|
||||
if (this.graphqlClient?.credential !== credential) {
|
||||
this.graphqlClient = { credential, client: createGraphqlClient(authenticationInfo) };
|
||||
}
|
||||
return this.graphqlClient.client;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param requestTimeoutMs caps the actor lookup when no actor is cached yet. Callers that must
|
||||
* not delay a command past a deadline pass it; the default has no timeout. A capped lookup
|
||||
|
||||
@@ -5,10 +5,7 @@ import path from 'path';
|
||||
import { ApiV2Error } from '../../ApiV2Error';
|
||||
import { AnalyticsWithOrchestration } from '../../analytics/AnalyticsManager';
|
||||
import { ApiV2Client } from '../../api';
|
||||
import {
|
||||
ExpoGraphqlClient,
|
||||
createGraphqlClient,
|
||||
} from '../../commandUtils/context/contextUtils/createGraphqlClient';
|
||||
import { createGraphqlClient } from '../../commandUtils/context/contextUtils/createGraphqlClient';
|
||||
import { UserQuery } from '../../graphql/queries/UserQuery';
|
||||
import Log from '../../log';
|
||||
import { promptAsync } from '../../prompts';
|
||||
@@ -116,28 +113,6 @@ describe(SessionManager, () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('getGraphqlClient', () => {
|
||||
beforeEach(() => {
|
||||
jest.mocked(createGraphqlClient).mockImplementation(() => ({}) as ExpoGraphqlClient);
|
||||
});
|
||||
|
||||
it('returns the same client for the same credential', () => {
|
||||
const sessionManager = new SessionManager(analytics);
|
||||
const client = sessionManager.getGraphqlClient({ accessToken: 'token', sessionSecret: null });
|
||||
expect(
|
||||
sessionManager.getGraphqlClient({ accessToken: 'token', sessionSecret: 'secret' })
|
||||
).toBe(client);
|
||||
});
|
||||
|
||||
it('returns a new client when the credential changes', () => {
|
||||
const sessionManager = new SessionManager(analytics);
|
||||
const client = sessionManager.getGraphqlClient({ accessToken: null, sessionSecret: 'a' });
|
||||
expect(sessionManager.getGraphqlClient({ accessToken: null, sessionSecret: 'b' })).not.toBe(
|
||||
client
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getUserAsync', () => {
|
||||
it('skips fetching user without access token or session secret', async () => {
|
||||
const sessionManager = new SessionManager(analytics);
|
||||
|
||||
@@ -207,27 +207,6 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"permissions": {
|
||||
"type": "object",
|
||||
"description": "Permissions of the EXPO_TOKEN that the build steps use. The user or robot that starts the build must have at least the requested role, or the build fails.",
|
||||
"markdownDescription": "Permissions of the `EXPO_TOKEN` that the build steps use. The user or robot that starts the build must have at least the requested role, or the build fails.",
|
||||
"additionalProperties": false,
|
||||
"required": ["account"],
|
||||
"properties": {
|
||||
"account": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["role"],
|
||||
"properties": {
|
||||
"role": {
|
||||
"description": "The account role of the EXPO_TOKEN.",
|
||||
"markdownDescription": "The account role of the `EXPO_TOKEN`.",
|
||||
"enum": ["runner", "developer", "release_manager"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"config": {
|
||||
"description": "Custom workflow file name that will be used to run this build. You can also specify this property on platform level for platform-specific workflows. Learn more: https://docs.expo.dev/custom-builds/get-started/",
|
||||
"type": "string"
|
||||
|
||||
@@ -800,85 +800,6 @@ test('Android-specific resourceClass', async () => {
|
||||
).resolves.not.toThrow();
|
||||
});
|
||||
|
||||
test('valid eas.json with permissions.account.role', async () => {
|
||||
await fs.writeJson('/project/eas.json', {
|
||||
build: {
|
||||
production: {
|
||||
permissions: { account: { role: 'release_manager' } },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const accessor = EasJsonAccessor.fromProjectPath('/project');
|
||||
const iosProfile = await EasJsonUtils.getBuildProfileAsync(accessor, Platform.IOS, 'production');
|
||||
const androidProfile = await EasJsonUtils.getBuildProfileAsync(
|
||||
accessor,
|
||||
Platform.ANDROID,
|
||||
'production'
|
||||
);
|
||||
|
||||
expect(iosProfile.permissions).toEqual({ account: { role: 'release_manager' } });
|
||||
expect(androidProfile.permissions).toEqual({ account: { role: 'release_manager' } });
|
||||
});
|
||||
|
||||
test('permissions.account.role is inherited from the extended profile', async () => {
|
||||
await fs.writeJson('/project/eas.json', {
|
||||
build: {
|
||||
base: {
|
||||
permissions: { account: { role: 'runner' } },
|
||||
},
|
||||
production: {
|
||||
extends: 'base',
|
||||
},
|
||||
preview: {
|
||||
extends: 'base',
|
||||
permissions: { account: { role: 'developer' } },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const accessor = EasJsonAccessor.fromProjectPath('/project');
|
||||
const productionProfile = await EasJsonUtils.getBuildProfileAsync(
|
||||
accessor,
|
||||
Platform.IOS,
|
||||
'production'
|
||||
);
|
||||
const previewProfile = await EasJsonUtils.getBuildProfileAsync(accessor, Platform.IOS, 'preview');
|
||||
|
||||
expect(productionProfile.permissions).toEqual({ account: { role: 'runner' } });
|
||||
expect(previewProfile.permissions).toEqual({ account: { role: 'developer' } });
|
||||
});
|
||||
|
||||
test('invalid permissions.account.role', async () => {
|
||||
await fs.writeJson('/project/eas.json', {
|
||||
build: {
|
||||
production: {
|
||||
permissions: { account: { role: 'admin' } },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const accessor = EasJsonAccessor.fromProjectPath('/project');
|
||||
await expect(
|
||||
EasJsonUtils.getBuildProfileAsync(accessor, Platform.IOS, 'production')
|
||||
).rejects.toThrowError(/build.production.permissions.account.role.*must be one of/);
|
||||
});
|
||||
|
||||
test('permissions without account.role', async () => {
|
||||
await fs.writeJson('/project/eas.json', {
|
||||
build: {
|
||||
production: {
|
||||
permissions: { account: {} },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const accessor = EasJsonAccessor.fromProjectPath('/project');
|
||||
await expect(
|
||||
EasJsonUtils.getBuildProfileAsync(accessor, Platform.IOS, 'production')
|
||||
).rejects.toThrowError(/build.production.permissions.account.role.*is required/);
|
||||
});
|
||||
|
||||
test('build profile with platform-specific custom build config', async () => {
|
||||
await fs.writeJson('/project/eas.json', {
|
||||
build: {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import Joi from 'joi';
|
||||
import semver from 'semver';
|
||||
|
||||
import { JobExpoTokenRole, ResourceClass } from './types';
|
||||
import { ResourceClass } from './types';
|
||||
|
||||
const AllowedCommonResourceClasses: ResourceClass[] = [
|
||||
ResourceClass.DEFAULT,
|
||||
@@ -31,21 +31,10 @@ const CacheSchema = Joi.object({
|
||||
'Cannot provide both "cache.customPaths" and "cache.paths" - use "cache.paths"',
|
||||
});
|
||||
|
||||
const PermissionsSchema = Joi.object({
|
||||
account: Joi.object({
|
||||
role: Joi.string()
|
||||
.valid(...Object.values(JobExpoTokenRole))
|
||||
.required(),
|
||||
}).required(),
|
||||
});
|
||||
|
||||
const CommonBuildProfileSchema = Joi.object({
|
||||
// builder
|
||||
resourceClass: Joi.string().valid(...AllowedCommonResourceClasses),
|
||||
|
||||
// permissions of the EXPO_TOKEN used by the build
|
||||
permissions: PermissionsSchema,
|
||||
|
||||
// build environment
|
||||
env: Joi.object().pattern(Joi.string(), Joi.string().empty(null)),
|
||||
node: Joi.string().empty(null).custom(semverCheck),
|
||||
|
||||
@@ -20,18 +20,6 @@ export enum ResourceClass {
|
||||
M_LARGE = 'm-large',
|
||||
}
|
||||
|
||||
export enum JobExpoTokenRole {
|
||||
RUNNER = 'runner',
|
||||
DEVELOPER = 'developer',
|
||||
RELEASE_MANAGER = 'release_manager',
|
||||
}
|
||||
|
||||
export interface BuildProfilePermissions {
|
||||
account: {
|
||||
role: JobExpoTokenRole;
|
||||
};
|
||||
}
|
||||
|
||||
export type DistributionType = 'store' | 'internal';
|
||||
|
||||
export type IosEnterpriseProvisioning = 'adhoc' | 'universal';
|
||||
@@ -44,9 +32,6 @@ export interface CommonBuildProfile {
|
||||
// builder
|
||||
resourceClass?: ResourceClass;
|
||||
|
||||
// permissions of the EXPO_TOKEN used by the build
|
||||
permissions?: BuildProfilePermissions;
|
||||
|
||||
// build environment
|
||||
env?: Record<string, string>;
|
||||
node?: string;
|
||||
|
||||
@@ -5,12 +5,10 @@ export { Platform } from '@expo/eas-build-job';
|
||||
export {
|
||||
AndroidVersionAutoIncrement,
|
||||
BuildProfile,
|
||||
BuildProfilePermissions,
|
||||
CredentialsSource,
|
||||
DistributionType,
|
||||
IosEnterpriseProvisioning,
|
||||
IosVersionAutoIncrement,
|
||||
JobExpoTokenRole,
|
||||
ResourceClass,
|
||||
} from './build/types';
|
||||
export { EasJsonAccessor } from './accessor';
|
||||
|
||||
@@ -202,8 +202,11 @@ export class BuildStepGlobalContext {
|
||||
return new BuildStepContext(this, options);
|
||||
}
|
||||
|
||||
public markAsCheckedOut(): void {
|
||||
public markAsCheckedOut(logger: bunyan): void {
|
||||
this.didCheckOut = true;
|
||||
logger.info(
|
||||
`Changing default working directory to ${this.defaultWorkingDirectory} (was ${this.projectTargetDirectory})`
|
||||
);
|
||||
}
|
||||
|
||||
public get hasAnyPreviousStepFailed(): boolean {
|
||||
|
||||
@@ -38,7 +38,7 @@ import {
|
||||
parseLocalCompositeFunctionPath,
|
||||
} from './utils/localCompositeFunctions';
|
||||
|
||||
type ValidatedHooks = ReadonlyMap<HookKey, Step[]>;
|
||||
type ValidatedHooks = ReadonlyMap<HookKey, { anchorId: HookAnchorId; steps: Step[] }>;
|
||||
|
||||
export class StepsConfigParser extends AbstractConfigParser {
|
||||
private readonly steps: Step[];
|
||||
@@ -114,6 +114,7 @@ export class StepsConfigParser extends AbstractConfigParser {
|
||||
// step ids identical across the splicing→engine rollout.
|
||||
const buildSteps: BuildStep[] = [];
|
||||
const hooksByAnchorStep = new Map<BuildStep, AnchorHooks>();
|
||||
const seenAnchorIds = new Set<HookAnchorId>();
|
||||
|
||||
for (const stepConfig of validatedSteps) {
|
||||
const maybeFunctionGroup =
|
||||
@@ -134,6 +135,7 @@ export class StepsConfigParser extends AbstractConfigParser {
|
||||
if (anchorId === undefined) {
|
||||
continue;
|
||||
}
|
||||
seenAnchorIds.add(anchorId);
|
||||
const anchorHooks = await this.constructAnchorHooksAsync(
|
||||
anchorId,
|
||||
validatedHooks,
|
||||
@@ -160,6 +162,7 @@ export class StepsConfigParser extends AbstractConfigParser {
|
||||
'Hook anchors are not supported on local composite function steps.'
|
||||
);
|
||||
}
|
||||
seenAnchorIds.add(anchorId);
|
||||
const before = await this.constructHookSideEntriesAsync(
|
||||
anchorId,
|
||||
'before',
|
||||
@@ -187,6 +190,14 @@ export class StepsConfigParser extends AbstractConfigParser {
|
||||
}
|
||||
}
|
||||
|
||||
for (const [hookKey, { anchorId }] of validatedHooks) {
|
||||
if (!seenAnchorIds.has(anchorId)) {
|
||||
this.ctx.baseLogger.warn(
|
||||
`Ignoring "hooks.${hookKey}": this build does not run the "${anchorId}" step.`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
buildSteps,
|
||||
buildFunctionById,
|
||||
@@ -195,13 +206,14 @@ export class StepsConfigParser extends AbstractConfigParser {
|
||||
}
|
||||
|
||||
private validateHooks(): ValidatedHooks {
|
||||
const validatedHooks = new Map<HookKey, Step[]>();
|
||||
const validatedHooks = new Map<HookKey, { anchorId: HookAnchorId; steps: Step[] }>();
|
||||
for (const [hookKey, hookSteps] of Object.entries(this.hooks)) {
|
||||
// A worker must not fail on a hook key newer than itself, so unregistered
|
||||
// keys skip validation entirely (their steps may reference functions this
|
||||
// worker lacks).
|
||||
const parsed = parseHookKey(hookKey);
|
||||
if (parsed === null) {
|
||||
this.ctx.baseLogger.warn(`Ignoring unknown hook key "${hookKey}".`);
|
||||
continue;
|
||||
}
|
||||
// An empty array is a deliberate no-op (e.g. opting out of a default);
|
||||
@@ -211,7 +223,10 @@ export class StepsConfigParser extends AbstractConfigParser {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
validatedHooks.set(`${parsed.side}_${parsed.anchorId}`, validateSteps(hookSteps));
|
||||
validatedHooks.set(`${parsed.side}_${parsed.anchorId}`, {
|
||||
anchorId: parsed.anchorId,
|
||||
steps: validateSteps(hookSteps),
|
||||
});
|
||||
} catch (err) {
|
||||
throw new BuildConfigError(
|
||||
`Invalid steps in "hooks.${hookKey}": ${err instanceof Error ? err.message : String(err)}`
|
||||
@@ -271,7 +286,7 @@ export class StepsConfigParser extends AbstractConfigParser {
|
||||
validatedHooks: ValidatedHooks,
|
||||
compositeFunctionExpander: CompositeFunctionExpander
|
||||
): Promise<HookEntry[]> {
|
||||
const hookSteps = validatedHooks.get(`${side}_${anchorId}`);
|
||||
const hookSteps = validatedHooks.get(`${side}_${anchorId}`)?.steps;
|
||||
if (hookSteps === undefined) {
|
||||
return [];
|
||||
}
|
||||
|
||||
@@ -130,7 +130,7 @@ describe(BuildStep, () => {
|
||||
projectTargetDirectory: '/a/b',
|
||||
relativeWorkingDirectory: 'c',
|
||||
});
|
||||
ctx.markAsCheckedOut();
|
||||
ctx.markAsCheckedOut(ctx.baseLogger);
|
||||
|
||||
const id = 'test1';
|
||||
const command = 'ls -la';
|
||||
@@ -150,7 +150,7 @@ describe(BuildStep, () => {
|
||||
projectTargetDirectory: '/a/b',
|
||||
relativeWorkingDirectory: 'c',
|
||||
});
|
||||
ctx.markAsCheckedOut();
|
||||
ctx.markAsCheckedOut(ctx.baseLogger);
|
||||
|
||||
const id = 'test1';
|
||||
const command = 'ls -la';
|
||||
|
||||
@@ -65,7 +65,7 @@ describe(BuildStepGlobalContext, () => {
|
||||
),
|
||||
false
|
||||
);
|
||||
ctx.markAsCheckedOut();
|
||||
ctx.markAsCheckedOut(ctx.baseLogger);
|
||||
expect(ctx.defaultWorkingDirectory).toBe(workingDirectory);
|
||||
});
|
||||
});
|
||||
@@ -136,7 +136,7 @@ describe(BuildStepGlobalContext, () => {
|
||||
},
|
||||
createMockLogger()
|
||||
);
|
||||
ctx.markAsCheckedOut();
|
||||
ctx.markAsCheckedOut(ctx.baseLogger);
|
||||
expect(ctx.stepsInternalBuildDirectory).toBe('/m/n/o');
|
||||
expect(ctx.defaultWorkingDirectory).toBe('/g/h/i');
|
||||
expect(ctx.runtimePlatform).toBe(BuildRuntimePlatform.DARWIN);
|
||||
@@ -199,7 +199,7 @@ describe(BuildStepGlobalContext, () => {
|
||||
const ctx = createGlobalContextMock({
|
||||
relativeWorkingDirectory: 'apps/mobile',
|
||||
});
|
||||
ctx.markAsCheckedOut();
|
||||
ctx.markAsCheckedOut(ctx.baseLogger);
|
||||
|
||||
const relativeChildCtx = ctx.stepCtx({
|
||||
relativeWorkingDirectory: 'scripts',
|
||||
@@ -230,7 +230,7 @@ describe(BuildStepGlobalContext, () => {
|
||||
projectTargetDirectory: tempDir,
|
||||
relativeWorkingDirectory: '',
|
||||
});
|
||||
ctx.markAsCheckedOut();
|
||||
ctx.markAsCheckedOut(ctx.baseLogger);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
|
||||
@@ -283,6 +283,9 @@ describe('StepsConfigParser hook construction', () => {
|
||||
});
|
||||
expect(orderedDisplayNames(workflow)).toEqual(['Checkout']);
|
||||
expect(workflow.hooksByAnchorStep.size).toBe(0);
|
||||
expect(ctx.baseLogger.warn).toHaveBeenCalledWith(
|
||||
'Ignoring "hooks.before_install_node_modules": this build does not run the "install_node_modules" step.'
|
||||
);
|
||||
});
|
||||
|
||||
it('rejects a hook step using a composite function missing from the catalog with BuildConfigError', async () => {
|
||||
@@ -855,17 +858,26 @@ describe('StepsConfigParser lazy hook composite loading', () => {
|
||||
expect(orderedDisplayNames(workflow)).toEqual(['Checkout']);
|
||||
});
|
||||
|
||||
it('ignores unknown and unrun hook keys without warning', async () => {
|
||||
const workflow = await parseWorkflowAsync({
|
||||
it('warns for a registered hook key whose anchor never appeared', async () => {
|
||||
await parseWorkflowAsync({
|
||||
ctx,
|
||||
steps: [{ uses: 'eas/checkout' }],
|
||||
hooks: {
|
||||
before_install_node_modules: [{ run: 'echo never' }],
|
||||
before_install_node_module: [{ run: 'echo typo' }],
|
||||
},
|
||||
hooks: { before_install_node_modules: [{ run: 'echo never' }] },
|
||||
});
|
||||
expect(workflow.hooksByAnchorStep.size).toBe(0);
|
||||
expect(ctx.baseLogger.warn).not.toHaveBeenCalled();
|
||||
expect(ctx.baseLogger.warn).toHaveBeenCalledWith(
|
||||
'Ignoring "hooks.before_install_node_modules": this build does not run the "install_node_modules" step.'
|
||||
);
|
||||
});
|
||||
|
||||
it('warns for an unknown hook key', async () => {
|
||||
await parseWorkflowAsync({
|
||||
ctx,
|
||||
steps: [{ uses: 'eas/checkout' }],
|
||||
hooks: { before_install_node_module: [{ run: 'echo typo' }] },
|
||||
});
|
||||
expect(ctx.baseLogger.warn).toHaveBeenCalledWith(
|
||||
'Ignoring unknown hook key "before_install_node_module".'
|
||||
);
|
||||
});
|
||||
|
||||
it('wraps a loader failure in a BuildConfigError naming the before-side hook key', async () => {
|
||||
|
||||
@@ -42,3 +42,18 @@ Github action builds a docker image for every PR with any change in `infra/eas-b
|
||||
### VM template
|
||||
|
||||
Github action builds a new vm template for every PR with any change in `infra/eas-build-worker-images/ios/{{ templateName }}`.
|
||||
|
||||
### Packaged startup
|
||||
|
||||
Existing services run `src/services/worker/dist/main.js`. In the worker archive,
|
||||
that path points to `bin/bootstrap.cjs`. The bootstrap loads only Node built-ins
|
||||
and starts `bin/start-worker.sh`. The shell then uses `exec` to start the normal
|
||||
worker with the original Node binary, options, and arguments.
|
||||
|
||||
Put setup that must precede worker imports or Node startup options in the shell.
|
||||
The bootstrap forwards SIGINT, SIGTERM, IPC messages, and the worker exit status.
|
||||
It remains the supervised parent process; test service shutdown on both platforms
|
||||
before deployment. A failure in the startup shell prevents the worker from
|
||||
loading. Direct execution of `packages/worker/dist/main.js` bypasses this hook.
|
||||
|
||||
Run `yarn workspace @expo/worker test:startup` for the process-level checks.
|
||||
@@ -0,0 +1,53 @@
|
||||
// This entry point must only load Node built-ins before the startup shell runs.
|
||||
const { spawn } = require('node:child_process');
|
||||
const path = require('node:path');
|
||||
|
||||
const worker = spawn(
|
||||
'/bin/bash',
|
||||
[
|
||||
path.join(__dirname, 'start-worker.sh'),
|
||||
process.execPath,
|
||||
...process.execArgv,
|
||||
path.join(__dirname, '../dist/main.js'),
|
||||
...process.argv.slice(2),
|
||||
],
|
||||
{ stdio: process.send ? ['inherit', 'inherit', 'inherit', 'ipc'] : 'inherit' }
|
||||
);
|
||||
|
||||
const signals = ['SIGINT', 'SIGTERM'];
|
||||
for (const signal of signals) {
|
||||
process.on(signal, () => worker.kill(signal));
|
||||
}
|
||||
|
||||
// Retain the readiness channel used by process managers in development.
|
||||
if (process.send) {
|
||||
worker.on('message', message => {
|
||||
if (process.connected) {
|
||||
process.send(message, () => {});
|
||||
}
|
||||
});
|
||||
process.on('message', message => {
|
||||
if (worker.connected) {
|
||||
worker.send(message, () => {});
|
||||
}
|
||||
});
|
||||
process.on('disconnect', () => {
|
||||
if (worker.connected) {
|
||||
worker.disconnect();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
worker.on('error', error => {
|
||||
process.stderr.write(`Worker startup failed: ${error.stack ?? error}\n`, () => process.exit(1));
|
||||
});
|
||||
worker.on('exit', (code, signal) => {
|
||||
if (signal) {
|
||||
for (const forwarded of signals) {
|
||||
process.removeAllListeners(forwarded);
|
||||
}
|
||||
process.kill(process.pid, signal);
|
||||
} else {
|
||||
process.exit(code ?? 1);
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,37 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# Direct local development must not change the machine's Java installation.
|
||||
case "${ENVIRONMENT:-}" in
|
||||
development|test) exec "$@" ;;
|
||||
staging|production) ;;
|
||||
*) echo 'Worker startup requires a known ENVIRONMENT' >&2; exit 1 ;;
|
||||
esac
|
||||
|
||||
here=$(cd "$(dirname "$0")" && pwd)
|
||||
case "${NODE_OPTIONS:-}" in
|
||||
*--use-bundled-ca*|*--use-system-ca*) echo 'Conflicting Node CA options' >&2; exit 1 ;;
|
||||
esac
|
||||
[ -z "${NODE_EXTRA_CA_CERTS:-}" ] || { echo 'Extra Node CA certificates require review' >&2; exit 1; }
|
||||
|
||||
export SSL_CERT_FILE="$here/tls/ca.pem"
|
||||
export SSL_CERT_DIR="$here/tls/empty"
|
||||
export CURL_CA_BUNDLE="$SSL_CERT_FILE"
|
||||
export REQUESTS_CA_BUNDLE="$SSL_CERT_FILE"
|
||||
export NODE_OPTIONS="${NODE_OPTIONS:+$NODE_OPTIONS }--use-openssl-ca"
|
||||
[ -s "$SSL_CERT_FILE" ] && [ -d "$SSL_CERT_DIR" ]
|
||||
|
||||
case "$(uname -s)" in
|
||||
Darwin) java_root=/Library/Java/JavaVirtualMachines ;;
|
||||
Linux) java_root=/usr/lib/jvm ;;
|
||||
*) echo 'Unsupported worker OS' >&2; exit 1 ;;
|
||||
esac
|
||||
|
||||
# Only the Java blocklist update needs root. The worker keeps its original user.
|
||||
if [ "$EUID" -eq 0 ]; then
|
||||
/bin/bash "$here/tls/block-java-certificates.sh" "$java_root"
|
||||
else
|
||||
sudo -n /bin/bash "$here/tls/block-java-certificates.sh" "$java_root"
|
||||
fi
|
||||
|
||||
exec "$@"
|
||||
@@ -0,0 +1,57 @@
|
||||
# Temporary eMudhra trust policy
|
||||
|
||||
The packaged startup shell applies this policy before the normal worker starts.
|
||||
It uses the included CA bundle for Node, OpenSSL clients, curl, and Requests.
|
||||
It adds the affected certificate fingerprints to Java blocklists. Only that
|
||||
Java update needs root; the worker continues as its original user.
|
||||
|
||||
No startup download or image rebuild is required. The tarball must include
|
||||
`bin/tls/empty/.gitkeep`: this directory replaces the system hashed CA directory.
|
||||
Development and test environments skip the policy. Staging and production stop
|
||||
startup if setup fails or inherited Node trust options conflict.
|
||||
|
||||
## Pinned data
|
||||
|
||||
`ca.pem` is the Mozilla CA bundle from https://curl.se/ca/cacert.pem, retrieved
|
||||
on 2026-10-22. Its header identifies the source snapshot as 2026-10-20.
|
||||
Source and bundled SHA-256:
|
||||
`796f1389bfea13c0ed78f663b765baaff54a508e5483786bd6ece73c5e3e3cbe`.
|
||||
The bundle already excludes all four affected root public keys. No certificates
|
||||
were removed from the downloaded file. Original source comments are retained.
|
||||
|
||||
`blocked-certs.txt` contains SHA-256 fingerprints for the four roots and 16
|
||||
intermediate certificates published at:
|
||||
|
||||
- https://emudhra.we-have-a-problem.com/roots.json
|
||||
- https://emudhra.we-have-a-problem.com/impacted-intermediates.json
|
||||
|
||||
The test fixtures retain those source records and the 12 published TLS chains
|
||||
from https://emudhra.we-have-a-problem.com/chains.zip. All were retrieved on
|
||||
2026-10-22. No private keys are included. Changes to these files are security
|
||||
policy changes and need review.
|
||||
|
||||
## Verification and limits
|
||||
|
||||
`yarn workspace @expo/worker test:startup` checks all 12 chains, a real local
|
||||
Node HTTPS request through the packaged entry point, setup failure, and Java
|
||||
blocklist preservation. Tests do not use real sudo or change the host JDKs.
|
||||
`test:unit` also checks trust-variable inheritance into the job environment.
|
||||
|
||||
Before deployment, run the packaged worker on every supported Linux and macOS
|
||||
image. Verify passwordless `sudo -n`, process shutdown, the worker connection,
|
||||
all installed Java versions, Ruby, curl, Python, and valid HTTPS controls.
|
||||
OpenSSL 3 is required for the offline chain tests, not for the worker startup.
|
||||
|
||||
This is a mitigation for the default worker and inherited job trust settings.
|
||||
It does not cover:
|
||||
|
||||
- The earlier worker-tarball download or host-side network requests.
|
||||
- Native macOS Keychain clients or a curl build that uses another trust backend.
|
||||
- Jobs that replace trust settings, use their own CA file, or install another JDK.
|
||||
- Separate container trust stores or JDKs outside the standard scanned directories.
|
||||
|
||||
The shared bundle can omit image-specific roots. Removing AAA and emSign roots
|
||||
can break valid services. Review these compatibility effects before rollout.
|
||||
Java file changes are tested; Java runtime rejection on each image is still
|
||||
required. A partial setup failure must discard the VM. Replace existing workers
|
||||
to apply or roll back the policy; do not remove it from a running worker.
|
||||
@@ -0,0 +1,22 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
here=$(cd "$(dirname "$0")" && pwd)
|
||||
java_root=$1
|
||||
[ -s "$here/blocked-certs.txt" ]
|
||||
|
||||
for security in "$java_root"/*/{lib/security,jre/lib/security,Contents/Home/lib/security,Contents/Home/jre/lib/security}; do
|
||||
[ -d "$security" ] || continue
|
||||
blocklist="$security/blocked.certs"
|
||||
case "$security" in */jre/lib/security) blocklist="$security/blacklisted.certs" ;; esac
|
||||
[ ! -f "$security/blacklisted.certs" ] || blocklist="$security/blacklisted.certs"
|
||||
if [ ! -f "$blocklist" ]; then printf 'Algorithm=SHA-256\n' > "$blocklist"; fi
|
||||
algorithm=$(sed -n 's/^[[:space:]]*Algorithm[[:space:]]*=[[:space:]]*//p' "$blocklist" | tr -d '\r')
|
||||
[ "$algorithm" = SHA-256 ] || { echo "Unsupported Java blocklist: $blocklist" >&2; exit 1; }
|
||||
[ -f "$blocklist.before-eas-tls-policy" ] || cp -p "$blocklist" "$blocklist.before-eas-tls-policy"
|
||||
[ -z "$(tail -c 1 "$blocklist")" ] || printf '\n' >> "$blocklist"
|
||||
while IFS= read -r fingerprint; do
|
||||
grep -Fxiq "$fingerprint" "$blocklist" || printf '%s\n' "$fingerprint" >> "$blocklist"
|
||||
done < "$here/blocked-certs.txt"
|
||||
echo "Updated Java blocklist: $blocklist"
|
||||
done
|
||||
@@ -0,0 +1,20 @@
|
||||
0164D7CEAE5BB909C272AF151FF44B1177AE9B47D9424B94F1D9558C30934D19
|
||||
42C4ACCD12C4F59597925E1A08A7A90102608543809308D50EC427E118BA1493
|
||||
432EC25F0D28DE9006D97BF752F17BBB2CD41F669933FCB072D9A1148D0261F3
|
||||
45A883F349AEDFD5F3A649B5498FFB121712BA8B448F3F2A91DBC5A766FCDDBE
|
||||
4B2460A466F95FD40113BCDCCA2CEB29B3B093921DE45474DA7DF48874C630B1
|
||||
579480AEA166290BD3C643589E795FC0D0E54E86505D180F1DFF99EFD3199512
|
||||
5F1DED4DD590AF74631290CF073B122A2AAC6A1FA479778A0BD43579BD832699
|
||||
62B7F9868263F72F6911FC78CEAF72C1979158B7368BF648683BB2AA39B02E72
|
||||
7485B51C0ED192878FA1D2F948FB529AE1842CC5086C4B67F7BE5B2039DE9489
|
||||
7DD78D5F4F13459A83DFF9ABBBA62EDBAF6F2D102BF257FD712F4D9F2746ED8D
|
||||
84112972B8E59D14560AE7E7119654474AAD119990CF3722BFC1E3B91615E1FD
|
||||
86A1ECBA089C4A8D3BBE2734C612BA341D813E043CF9E8A862CD5C57A36BBE6B
|
||||
98B896625C0C230ADEEC3EB2CF6AAD5001ECF9156A68644E82374CE5833BC25B
|
||||
9E39DE2881273ACC225A149C72BA3E0F51424963669AB633EE1DC7DF5DB067B4
|
||||
A496E23AC0CAB8155FC9B117B97EAE368EFCBB9041AEC8804D5A308D2D01C2D8
|
||||
CB6E15165AC802E9B3F993F16C25D41F4E81C21EBDE71AE1B012AC00BF9C7C3F
|
||||
D7A7A0FB5D7E2731D771E9484EBCDEF71D5F0C3E0A2948782BC83EE0EA699EF4
|
||||
E7568FE6E12331F4FD7DE61DA4C7559D0F8EB192A218D4741A3BA75184333838
|
||||
F2EDAAFF3D9B701FB4D14C9F285F8C0BAD90FC3BCC1FA07D47F4718EDABA34A5
|
||||
FD9D5C2DBBD4995AB69B8D50C439D9527B00529199C2FD01B2174EB58F07021F
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1 @@
|
||||
No hashed CA files belong in this directory. It replaces the system CA lookup directory.
|
||||
@@ -11,11 +11,12 @@
|
||||
"compile": "tsc -p tsconfig.build.json",
|
||||
"prepublishOnly": "npm run build",
|
||||
"test": "yarn test:unit",
|
||||
"test:unit": "./resources/jest --config jest.config.unit.ts",
|
||||
"test:unit": "yarn test:startup && ./resources/jest --config jest.config.unit.ts",
|
||||
"test:unit:watch": "./resources/jest --config jest.config.unit.ts --watch",
|
||||
"test:integration": "./resources/jest --runInBand --config jest.config.integration.ts",
|
||||
"test:integration:watch": "./resources/jest --runInBand --config jest.config.integration.ts --watchAll",
|
||||
"typecheck": "tsc"
|
||||
"typecheck": "tsc",
|
||||
"test:startup": "node --test tests/*.test.cjs"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/core": "7.24.7",
|
||||
|
||||
@@ -120,10 +120,10 @@ fi
|
||||
|
||||
# Create backward-compatible symlink for orchestrator
|
||||
# The orchestrator expects ./src/services/worker/dist/main.js
|
||||
# but the new structure has ./packages/worker/dist/main.js
|
||||
# Route it through the packaged startup wrapper before loading worker modules.
|
||||
# We're only symlinking the specific file to discover any other legacy path usage
|
||||
mkdir -p "$target_root_dir/src/services/worker/dist"
|
||||
ln -s ../../../../packages/worker/dist/main.js "$target_root_dir/src/services/worker/dist/main.js"
|
||||
ln -s ../../../../packages/worker/bin/bootstrap.cjs "$target_root_dir/src/services/worker/dist/main.js"
|
||||
|
||||
tar zcf $OUTPUT_FILE -C $target_root_dir .
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import config from '../config';
|
||||
import { createBuildContext } from '../context';
|
||||
import { getBuildEnv } from '../env';
|
||||
import { prepareRuntimeEnvironmentConfigFiles } from '../runtimeEnvironment';
|
||||
import { uploadSourceMapAsync, uploadWorkflowArtifactAsync } from '../upload';
|
||||
import { uploadSourceMapAsync } from '../upload';
|
||||
|
||||
jest.mock('../env', () => ({
|
||||
getBuildEnv: jest.fn(() => ({
|
||||
@@ -18,7 +18,6 @@ jest.mock('../runtimeEnvironment', () => ({
|
||||
}));
|
||||
jest.mock('../upload', () => ({
|
||||
...jest.requireActual('../upload'),
|
||||
uploadWorkflowArtifactAsync: jest.fn(async () => ({ artifactId: 'artifact-id' })),
|
||||
uploadSourceMapAsync: jest.fn(async () => ({ filename: null })),
|
||||
uploadWithAnalyticsAsync: jest.fn(async fn => await fn()),
|
||||
}));
|
||||
@@ -130,28 +129,4 @@ describe(createBuildContext.name, () => {
|
||||
logger: childLogger,
|
||||
});
|
||||
});
|
||||
it('uploads named artifacts to workflow storage and returns their ID', async () => {
|
||||
const ctx = await createBuildContext({
|
||||
...baseOptions,
|
||||
job: {
|
||||
type: Workflow.GENERIC,
|
||||
secrets: { environmentSecrets: [] },
|
||||
} as any,
|
||||
});
|
||||
const metadata = { kind: 'maestro-report' };
|
||||
|
||||
const result = await ctx.uploadArtifact({
|
||||
artifact: { type: null, name: 'Report', paths: ['/workingdir/report'], metadata },
|
||||
logger: childLogger,
|
||||
});
|
||||
|
||||
expect(uploadWorkflowArtifactAsync).toHaveBeenCalledWith(ctx, {
|
||||
artifactPaths: ['/workingdir/report'],
|
||||
name: 'Report',
|
||||
metadata,
|
||||
logger: childLogger,
|
||||
});
|
||||
expect(result).toEqual({ artifactId: 'artifact-id' });
|
||||
expect(ctx.artifacts).toEqual({});
|
||||
});
|
||||
});
|
||||
@@ -21,6 +21,10 @@ describe(getBuildEnv.name, () => {
|
||||
EAS_BUILD_COCOAPODS_CACHE_URL: process.env.EAS_BUILD_COCOAPODS_CACHE_URL,
|
||||
NPM_CONFIG_REGISTRY: process.env.NPM_CONFIG_REGISTRY,
|
||||
NODE_OPTIONS: process.env.NODE_OPTIONS,
|
||||
SSL_CERT_FILE: process.env.SSL_CERT_FILE,
|
||||
SSL_CERT_DIR: process.env.SSL_CERT_DIR,
|
||||
CURL_CA_BUNDLE: process.env.CURL_CA_BUNDLE,
|
||||
REQUESTS_CA_BUNDLE: process.env.REQUESTS_CA_BUNDLE,
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -51,6 +55,10 @@ describe(getBuildEnv.name, () => {
|
||||
restoreEnv('EAS_BUILD_COCOAPODS_CACHE_URL', originalCacheUrls.EAS_BUILD_COCOAPODS_CACHE_URL);
|
||||
restoreEnv('NPM_CONFIG_REGISTRY', originalCacheUrls.NPM_CONFIG_REGISTRY);
|
||||
restoreEnv('NODE_OPTIONS', originalCacheUrls.NODE_OPTIONS);
|
||||
restoreEnv('SSL_CERT_FILE', originalCacheUrls.SSL_CERT_FILE);
|
||||
restoreEnv('SSL_CERT_DIR', originalCacheUrls.SSL_CERT_DIR);
|
||||
restoreEnv('CURL_CA_BUNDLE', originalCacheUrls.CURL_CA_BUNDLE);
|
||||
restoreEnv('REQUESTS_CA_BUNDLE', originalCacheUrls.REQUESTS_CA_BUNDLE);
|
||||
mockProcessPlatform(originalPlatform);
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
@@ -331,8 +339,15 @@ describe(getBuildEnv.name, () => {
|
||||
expect(getNodeMemoryOptions()).toBeUndefined();
|
||||
});
|
||||
|
||||
it('keeps Node.js options inherited from the worker environment in override position', () => {
|
||||
process.env.NODE_OPTIONS = '--max-old-space-size=2048 --enable-source-maps';
|
||||
it('keeps worker trust settings and inherited Node.js options', () => {
|
||||
process.env.NODE_OPTIONS = '--max-old-space-size=2048 --enable-source-maps --use-openssl-ca';
|
||||
const trust = {
|
||||
SSL_CERT_FILE: '/worker/bin/tls/ca.pem',
|
||||
SSL_CERT_DIR: '/worker/bin/tls/empty',
|
||||
CURL_CA_BUNDLE: '/worker/bin/tls/ca.pem',
|
||||
REQUESTS_CA_BUNDLE: '/worker/bin/tls/ca.pem',
|
||||
};
|
||||
Object.assign(process.env, trust);
|
||||
jest.spyOn(process, 'constrainedMemory').mockReturnValue(6 * 1024 ** 3);
|
||||
jest.spyOn(v8, 'getHeapStatistics').mockReturnValue({
|
||||
heap_size_limit: 2 * 1024 ** 3,
|
||||
@@ -357,8 +372,9 @@ describe(getBuildEnv.name, () => {
|
||||
});
|
||||
|
||||
expect(env.NODE_OPTIONS).toBe(
|
||||
'--max-old-space-size=3072 --max-old-space-size=2048 --enable-source-maps'
|
||||
'--max-old-space-size=3072 --max-old-space-size=2048 --enable-source-maps --use-openssl-ca'
|
||||
);
|
||||
expect(env).toEqual(expect.objectContaining(trust));
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -18,7 +18,9 @@ jest.mock('@expo/build-tools', () => {
|
||||
MockRemoteLoggerStream.instances.push(this);
|
||||
}
|
||||
|
||||
public async init(): Promise<void> {}
|
||||
public async init(): Promise<string> {
|
||||
return 'https://gcs.expo.test/logs';
|
||||
}
|
||||
|
||||
public async cleanUp(): Promise<void> {}
|
||||
|
||||
|
||||
@@ -111,7 +111,6 @@ describe('prepareRuntimeEnvironment', () => {
|
||||
describe('installNode', () => {
|
||||
describe('prepareRuntimeEnvironment', () => {
|
||||
beforeEach(() => {
|
||||
jest.spyOn(RuntimeSettings, 'getNodeJsCacheUrl').mockReturnValue('https://node.example');
|
||||
jest.mocked(pathExists).mockResolvedValue(true);
|
||||
|
||||
jest.mocked(spawn).mockImplementation((cmd, _args, opts) => {
|
||||
@@ -149,104 +148,6 @@ describe('prepareRuntimeEnvironment', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it.each([
|
||||
{
|
||||
mirror: 'https://node.example',
|
||||
failures: 0,
|
||||
attempts: ['https://node.example'],
|
||||
succeeds: true,
|
||||
},
|
||||
{
|
||||
mirror: 'https://node.example',
|
||||
failures: 1,
|
||||
attempts: ['https://node.example', undefined],
|
||||
succeeds: true,
|
||||
},
|
||||
{
|
||||
mirror: 'https://node.example',
|
||||
failures: 2,
|
||||
attempts: ['https://node.example', undefined],
|
||||
succeeds: false,
|
||||
},
|
||||
{
|
||||
mirror: 'https://private.example',
|
||||
failures: 1,
|
||||
attempts: ['https://private.example'],
|
||||
succeeds: false,
|
||||
},
|
||||
{
|
||||
mirror: 'https://node.example',
|
||||
cacheUrl: null,
|
||||
failures: 1,
|
||||
attempts: ['https://node.example'],
|
||||
succeeds: false,
|
||||
},
|
||||
{ mirror: undefined, failures: 1, attempts: [undefined], succeeds: false },
|
||||
{ mirror: '', failures: 1, attempts: [''], succeeds: false },
|
||||
])(
|
||||
'handles Node.js install with mirror=$mirror and failures=$failures',
|
||||
async ({ mirror, failures, attempts, succeeds, cacheUrl = 'https://node.example' }) => {
|
||||
jest.mocked(RuntimeSettings.getNodeJsCacheUrl).mockReturnValue(cacheUrl);
|
||||
const buildContext = {
|
||||
...ctx,
|
||||
env: { ...ctx.env, PATH: '/usr/bin', NVM_NODEJS_ORG_MIRROR: mirror },
|
||||
};
|
||||
const installMirrors: (string | undefined)[] = [];
|
||||
jest.mocked(spawn).mockImplementation(async (cmd, args, opts) => {
|
||||
if (cmd === 'bash' && args[1].includes('nvm install')) {
|
||||
installMirrors.push(opts?.env?.NVM_NODEJS_ORG_MIRROR);
|
||||
if (installMirrors.length <= failures) {
|
||||
throw new Error('Node.js download failed');
|
||||
}
|
||||
return { ...spawnResult, stdout: 'Now using node v16.20.1 (npm v8.19.4)' };
|
||||
}
|
||||
return spawnResult;
|
||||
});
|
||||
|
||||
const preparation = prepareRuntimeEnvironment(buildContext, { node: 'v16' }, false);
|
||||
if (succeeds) {
|
||||
await preparation;
|
||||
expect(buildContext.env.PATH).toMatch(
|
||||
/\/\.nvm\/versions\/node\/v16\.20\.1\/bin:\/usr\/bin$/
|
||||
);
|
||||
expect(spawn).toHaveBeenCalledWith(
|
||||
'bash',
|
||||
['-c', 'source ~/.nvm/nvm.sh && nvm alias default 16.20.1'],
|
||||
expect.anything()
|
||||
);
|
||||
expect(ctx.logger.error).not.toHaveBeenCalled();
|
||||
} else {
|
||||
await expect(preparation).rejects.toThrow('Failed to install Node.js.');
|
||||
expect(buildContext.env.PATH).toBe('/usr/bin');
|
||||
}
|
||||
expect(installMirrors).toStrictEqual(attempts);
|
||||
expect(buildContext.env.NVM_NODEJS_ORG_MIRROR).toBe(attempts[attempts.length - 1]);
|
||||
}
|
||||
);
|
||||
|
||||
it('does not retry Node.js installation when setting the default alias fails', async () => {
|
||||
const buildContext = {
|
||||
...ctx,
|
||||
env: { ...ctx.env, NVM_NODEJS_ORG_MIRROR: 'https://node.example' },
|
||||
};
|
||||
let installCount = 0;
|
||||
jest.mocked(spawn).mockImplementation(async (cmd, args) => {
|
||||
if (cmd === 'bash' && args[1].includes('nvm install')) {
|
||||
installCount++;
|
||||
}
|
||||
if (cmd === 'bash' && args[1].includes('nvm alias')) {
|
||||
throw new Error('Cannot write default alias');
|
||||
}
|
||||
return spawnResult;
|
||||
});
|
||||
|
||||
await expect(
|
||||
prepareRuntimeEnvironment(buildContext, { node: '16.20.1' }, false)
|
||||
).rejects.toThrow('Failed to install Node.js.');
|
||||
expect(installCount).toBe(1);
|
||||
expect(buildContext.env.NVM_NODEJS_ORG_MIRROR).toBe('https://node.example');
|
||||
});
|
||||
|
||||
it('installs Bun when a specified version is different from installed version', async () => {
|
||||
let isFirstTimeCheckingBunVersion = true;
|
||||
jest.mocked(spawn).mockImplementation((cmd, _args, _opts) => {
|
||||
|
||||
@@ -136,32 +136,15 @@ async function installNode(ctx: PreDownloadBuildContext, version: string): Promi
|
||||
let sanitizedVersion = version.startsWith('v') ? version.slice(1) : version;
|
||||
try {
|
||||
ctx.logger.info(`Installing node v${sanitizedVersion}`);
|
||||
const installAsync = async () => {
|
||||
return await spawn('bash', ['-c', `source ~/.nvm/nvm.sh && nvm install ${version}`], {
|
||||
const { stdout } = await spawn(
|
||||
'bash',
|
||||
['-c', `source ~/.nvm/nvm.sh && nvm install ${version}`],
|
||||
{
|
||||
logger: ctx.logger,
|
||||
env: ctx.env,
|
||||
});
|
||||
};
|
||||
let result;
|
||||
try {
|
||||
result = await installAsync();
|
||||
} catch (err) {
|
||||
const mirror = ctx.env.NVM_NODEJS_ORG_MIRROR;
|
||||
// Only bypass the internal mirror enabled by the runtime settings.
|
||||
if (!mirror || mirror !== RuntimeSettings.getNodeJsCacheUrl()) {
|
||||
throw err;
|
||||
}
|
||||
);
|
||||
|
||||
ctx.logger.warn(
|
||||
{ err },
|
||||
'Failed to install Node.js using the worker mirror. Removing NVM_NODEJS_ORG_MIRROR from the build environment for the rest of this build and retrying with the default download source.'
|
||||
);
|
||||
// Keep the failed mirror disabled for the rest of this build.
|
||||
delete ctx.env.NVM_NODEJS_ORG_MIRROR;
|
||||
result = await installAsync();
|
||||
}
|
||||
|
||||
const { stdout } = result;
|
||||
sanitizedVersion = stdout.match(/Now using node v(\d+\.\d+\.\d+)/)?.[1] ?? sanitizedVersion;
|
||||
await spawn('bash', ['-c', `source ~/.nvm/nvm.sh && nvm alias default ${sanitizedVersion}`], {
|
||||
logger: ctx.logger,
|
||||
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIICQDCCAcagAwIBAgIUU3mgjls/8ZJ8upmukdlSQR1IF/UwCgYIKoZIzj0EAwIw
|
||||
PzELMAkGA1UEBhMCTkwxEzARBgNVBAoMCmVNdWRocmEgQlYxGzAZBgNVBAMMEkVN
|
||||
IERWIFRMUyBDQSAtIEczQjAeFw0yNjEwMjAwMjIyNDFaFw0yNzA1MDMwMjI3NDFa
|
||||
MDMxMTAvBgNVBAMMKGR2LWczYi1hYWEtZW11ZGhyYS53ZS1oYXZlLWEtcHJvYmxl
|
||||
bS5jb20wWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAATK3sfcOxxtM2Vdq9Qbw/GR
|
||||
NsHlJcnqINEInXO3fkWhw/FYT80hkqGk0jSN/EKib3YZJsr5GyfkYcpEiA2rwMgm
|
||||
o4GrMIGoMAwGA1UdEwEB/wQCMAAwDgYDVR0PAQH/BAQDAgeAMBMGA1UdJQQMMAoG
|
||||
CCsGAQUFBwMBMDMGA1UdEQQsMCqCKGR2LWczYi1hYWEtZW11ZGhyYS53ZS1oYXZl
|
||||
LWEtcHJvYmxlbS5jb20wHQYDVR0OBBYEFDggfJFz1eeToKmzFBSYkMCvpAu0MB8G
|
||||
A1UdIwQYMBaAFOIeL+ccGTkOVUtiwSAkVzUkiiUUMAoGCCqGSM49BAMCA2gAMGUC
|
||||
MDqDDOt7K313QIH0rhn/ZKnzFu8+/9W8OlTuFtDfVCTRDfRmqdyfsCfLsEk7xqHq
|
||||
uQIxAJqyLERABmU98U46OCuT5bp5h0ytuSEIGM7orTpKci5S1u1B/AOmwv162U7R
|
||||
TcVPHA==
|
||||
-----END CERTIFICATE-----
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIDzjCCAragAwIBAgIRAKAk/fS6Zlx4XOSgIEg4WLcwDQYJKoZIhvcNAQELBQAw
|
||||
ezELMAkGA1UEBhMCR0IxGzAZBgNVBAgMEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4G
|
||||
A1UEBwwHU2FsZm9yZDEaMBgGA1UECgwRQ29tb2RvIENBIExpbWl0ZWQxITAfBgNV
|
||||
BAMMGEFBQSBDZXJ0aWZpY2F0ZSBTZXJ2aWNlczAeFw0yNTA1MTQwMDAwMDBaFw0y
|
||||
ODEyMzEyMzU5NTlaMD8xCzAJBgNVBAYTAk5MMRMwEQYDVQQKDAplTXVkaHJhIEJW
|
||||
MRswGQYDVQQDDBJFTSBEViBUTFMgQ0EgLSBHM0IwdjAQBgcqhkjOPQIBBgUrgQQA
|
||||
IgNiAATnntnASpdzxHNeUREdqFrCvV5kqzdLgOEsvCA47B5PDjuBL8mFnh5nr7BO
|
||||
L4vJf3SFumWqkR7opORbYtXMA9Vr5nizw4y03JN8okYOI4RTyJKTXSnUT5HhVga2
|
||||
PM7atGijggE1MIIBMTAfBgNVHSMEGDAWgBSgEQojPpbxB+zirynvgqV/0DCktDAd
|
||||
BgNVHQ4EFgQU4h4v5xwZOQ5VS2LBICRXNSSKJRQwDgYDVR0PAQH/BAQDAgEGMBIG
|
||||
A1UdEwEB/wQIMAYBAf8CAQAwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMC
|
||||
MDEGA1UdIAQqMCgwCAYGZ4EMAQIBMA0GCysGAQQBg44hAQABMA0GCysGAQQBg44h
|
||||
AQJkMEMGA1UdHwQ8MDowOKA2oDSGMmh0dHA6Ly9jcmwuY29tb2RvY2EuY29tL0FB
|
||||
QUNlcnRpZmljYXRlU2VydmljZXMuY3JsMDQGCCsGAQUFBwEBBCgwJjAkBggrBgEF
|
||||
BQcwAYYYaHR0cDovL29jc3AuY29tb2RvY2EuY29tMA0GCSqGSIb3DQEBCwUAA4IB
|
||||
AQBzQ1IoqpdotM6gFrR4Q3YrKifOLeaj5XXunldpVE/vZn2TxC2y0s8oYiLR5ICE
|
||||
wqVXSB7N6BM1Mp+Nif7Jbhb2IswAfSR4Iik8+B0iRmi7lYoNlycCgHjmsjOmZaxM
|
||||
QkPmjXAJbG5f4V41OsvJZmS7lI3r0IuvEDVX3GAZ13on6IImSs9ybNpHQ7NCqkMQ
|
||||
7wH4uAO1dtpFycgJSdHaCOhHjrox2BTYGLdpUrYwY3EOAjlZ8UGgsxs7mG+xquMl
|
||||
bN1LFURwfnkVMP3T/DBIZ/ys2+lNFyxK3FlArrV4/000uTnVB1lO8oGORYdXoT5V
|
||||
GUyRCEIDhF5TBUuKF4E3kf1h
|
||||
-----END CERTIFICATE-----
|
||||
@@ -0,0 +1,56 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIICOTCCAb6gAwIBAgIUY1R/PHrtXShV0uhLJ5EyjsHvMi0wCgYIKoZIzj0EAwIw
|
||||
PzELMAkGA1UEBhMCTkwxEzARBgNVBAoMCmVNdWRocmEgQlYxGzAZBgNVBAMMEkVN
|
||||
IERWIFRMUyBDQSAtIEczQjAeFw0yNjEwMjAwMDU5MzZaFw0yNzA1MDMwMTA0MzZa
|
||||
MC8xLTArBgNVBAMMJGR2LWczYi1lbXVkaHJhLndlLWhhdmUtYS1wcm9ibGVtLmNv
|
||||
bTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABFpE24ACBFHQLzpg+7D767thJ3+Q
|
||||
NIoJJugUMT/OftEz6G6uDdxlS+9VfXYBDglHjJirjEPFNglOGJoQNQuiz7Gjgacw
|
||||
gaQwDAYDVR0TAQH/BAIwADAOBgNVHQ8BAf8EBAMCB4AwEwYDVR0lBAwwCgYIKwYB
|
||||
BQUHAwEwLwYDVR0RBCgwJoIkZHYtZzNiLWVtdWRocmEud2UtaGF2ZS1hLXByb2Js
|
||||
ZW0uY29tMB0GA1UdDgQWBBT4r5v4etYNuG21BqBqs273OkcHiDAfBgNVHSMEGDAW
|
||||
gBTiHi/nHBk5DlVLYsEgJFc1JIolFDAKBggqhkjOPQQDAgNpADBmAjEAlxNF+/8l
|
||||
APpgibawACy15yn5bcvTksz2Zw0R9z9nUK9e3MSSk+hXagg7LVb7V4NtAjEA4qyR
|
||||
laFPkaokrnTjqQieq8CgDQ6tIz3FjBvj3D7vkhKXHaml6zD2zT5ih2Y+lD2h
|
||||
-----END CERTIFICATE-----
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIDaDCCAu6gAwIBAgIQAKSu5njyM2K0wORgt3LyFDAKBggqhkjOPQQDAzBWMQsw
|
||||
CQYDVQQGEwJJTjElMCMGA1UECgwcZU11ZGhyYSBUZWNobm9sb2dpZXMgTGltaXRl
|
||||
ZDEgMB4GA1UEAwwXZW1TaWduIFJvb3QgVExTIENBIC0gRzMwHhcNMjUwNDMwMTU1
|
||||
NDA4WhcNNDAwNDI4MTgzMDAwWjA/MQswCQYDVQQGEwJOTDETMBEGA1UECgwKZU11
|
||||
ZGhyYSBCVjEbMBkGA1UEAwwSRU0gRFYgVExTIENBIC0gRzNCMHYwEAYHKoZIzj0C
|
||||
AQYFK4EEACIDYgAE557ZwEqXc8RzXlERHahawr1eZKs3S4DhLLwgOOweTw47gS/J
|
||||
hZ4eZ6+wTi+LyX90hbplqpEe6KTkW2LVzAPVa+Z4s8OMtNyTfKJGDiOEU8iSk10p
|
||||
1E+R4VYGtjzO2rRoo4IBljCCAZIwHwYDVR0jBBgwFoAU4TqrIXzx401UrtRyt/2L
|
||||
WUIgkFQwHQYDVR0OBBYEFOIeL+ccGTkOVUtiwSAkVzUkiiUUMB0GA1UdJQQWMBQG
|
||||
CCsGAQUFBwMBBggrBgEFBQcDAjBdBgNVHSAEVjBUMDkGCysGAQQBg44hAQABMCow
|
||||
KAYIKwYBBQUHAgEWHGh0dHA6Ly9yZXBvc2l0b3J5LmVtc2lnbi5jb20wDQYLKwYB
|
||||
BAGDjiEBAmQwCAYGZ4EMAQIBMHUGCCsGAQUFBwEBBGkwZzAkBggrBgEFBQcwAYYY
|
||||
aHR0cDovL29jc3AtYi5lbXNpZ24uY29tMD8GCCsGAQUFBzAChjNodHRwOi8vcmVw
|
||||
b3NpdG9yeS5lbXNpZ24uY29tL2NlcnRzL0VNRFZUTFNDQUczQi5wN2MwNwYDVR0f
|
||||
BDAwLjAsoCqgKIYmaHR0cDovL2NybC5lbXNpZ24uY29tLz9yb290dGxzY2FnMy5j
|
||||
cmwwDgYDVR0PAQH/BAQDAgEGMBIGA1UdEwEB/wQIMAYBAf8CAQAwCgYIKoZIzj0E
|
||||
AwMDaAAwZQIwLmsGXi6oFMJ2klkcEMXMug6gvxQWZWkewiDWVzQyl2L6z6DuQfPO
|
||||
Ibh1grvdULDCAjEAxZvLlxUavG69J6wnSjC8XBbqNIQsJ6CE2QdNOe84Ptxb6VG8
|
||||
gt2TlfJbHii+KNvK
|
||||
-----END CERTIFICATE-----
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIDdDCCAvmgAwIBAgIQAJYaPvsTWVdu0EAFtGhKWzAKBggqhkjOPQQDAzBrMQsw
|
||||
CQYDVQQGEwJJTjETMBEGA1UECxMKZW1TaWduIFBLSTElMCMGA1UEChMcZU11ZGhy
|
||||
YSBUZWNobm9sb2dpZXMgTGltaXRlZDEgMB4GA1UEAxMXZW1TaWduIEVDQyBSb290
|
||||
IENBIC0gRzMwHhcNMjQwNzExMTU0MzM2WhcNMzkwNzA5MTgzMDAwWjBWMQswCQYD
|
||||
VQQGEwJJTjElMCMGA1UECgwcZU11ZGhyYSBUZWNobm9sb2dpZXMgTGltaXRlZDEg
|
||||
MB4GA1UEAwwXZW1TaWduIFJvb3QgVExTIENBIC0gRzMwdjAQBgcqhkjOPQIBBgUr
|
||||
gQQAIgNiAARfD9w/q1cWxL6D9bMGH6CQ4u+gAV6GN9s+PwQx9Bl4hC+0iU8QOk8m
|
||||
WO/RwXQy10DCSxT8vSIJClHKp+Ao4qJKdYeV0SwZ7LMQziyLfxIUOY6Grja/sQFw
|
||||
LKYf8EdDXAyjggF1MIIBcTAfBgNVHSMEGDAWgBR8XQKEE9TMipuBzhccLikenEhj
|
||||
QjAdBgNVHQ4EFgQU4TqrIXzx401UrtRyt/2LWUIgkFQwHQYDVR0lBBYwFAYIKwYB
|
||||
BQUHAwEGCCsGAQUFBwMCMD0GA1UdIAQ2MDQwMgYEVR0gADAqMCgGCCsGAQUFBwIB
|
||||
FhxodHRwOi8vcmVwb3NpdG9yeS5lbXNpZ24uY29tMHgGCCsGAQUFBwEBBGwwajAi
|
||||
BggrBgEFBQcwAYYWaHR0cDovL29jc3AuZW1zaWduLmNvbTBEBggrBgEFBQcwAoY4
|
||||
aHR0cDovL3JlcG9zaXRvcnkuZW1zaWduLmNvbS9jZXJ0cy9lbVNpZ25FQ0NSb290
|
||||
Q0FHMy5jcnQwMwYDVR0fBCwwKjAooCagJIYiaHR0cDovL2NybC5lbXNpZ24uY29t
|
||||
P1Jvb3RDQUczLmNybDAOBgNVHQ8BAf8EBAMCAQYwEgYDVR0TAQH/BAgwBgEB/wIB
|
||||
ATAKBggqhkjOPQQDAwNpADBmAjEAt9CIpRGp46QHQYDUBnLKw+vvK0/h9vmilEAg
|
||||
TroUl/anH4x2rXHqiCtBTBfgKDegAjEA8WpG1LTZdAIL0FSYqxXeREaeTSxEofM0
|
||||
1jJH3jGvCXC79LU9UPJ2o3TU+UIj2gMV
|
||||
-----END CERTIFICATE-----
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIICQjCCAcegAwIBAgIUMH3z62Z6agEULBZqrfhpw9/ZpCAwCgYIKoZIzj0EAwIw
|
||||
QDELMAkGA1UEBhMCVVMxFDASBgNVBAoMC2VNdWRocmEgSW5jMRswGQYDVQQDDBJF
|
||||
TSBEViBUTFMgQ0EgLSBHM0MwHhcNMjYxMDIwMDIyMjQxWhcNMjcwNTAzMDIyNzQx
|
||||
WjAzMTEwLwYDVQQDDChkdi1nM2MtYWFhLWVtdWRocmEud2UtaGF2ZS1hLXByb2Js
|
||||
ZW0uY29tMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEAMPpHsQqEqIerrj/durO
|
||||
EN7gkUVJSVgIPN6+VJ2sEGXDyqXAN1fnNzGuASP/4WEr0RIhht3cYvuUYp2SYofr
|
||||
w6OBqzCBqDAMBgNVHRMBAf8EAjAAMA4GA1UdDwEB/wQEAwIHgDATBgNVHSUEDDAK
|
||||
BggrBgEFBQcDATAzBgNVHREELDAqgihkdi1nM2MtYWFhLWVtdWRocmEud2UtaGF2
|
||||
ZS1hLXByb2JsZW0uY29tMB0GA1UdDgQWBBT8CNQz4OLVnIwrQYZBF+++9jTCvzAf
|
||||
BgNVHSMEGDAWgBSufhBcPCmWwahHJwVZ8eA91OUK8zAKBggqhkjOPQQDAgNpADBm
|
||||
AjEAp3ZLdfBvKcZxgR2p8LZaxWzP6uB7zU/EZ9BA49Ds2P5ByYkRodwK5qcwt4J/
|
||||
VL/vAjEAnnxrcrDX7SzoJxuho604GBh6eyBLJLCdmcy+qb+LA3u5oxP/OjAzCA4x
|
||||
v1kw6Tm4
|
||||
-----END CERTIFICATE-----
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIDzzCCAregAwIBAgIRAJHzR/Glt9OZYrleDuTimdEwDQYJKoZIhvcNAQELBQAw
|
||||
ezELMAkGA1UEBhMCR0IxGzAZBgNVBAgMEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4G
|
||||
A1UEBwwHU2FsZm9yZDEaMBgGA1UECgwRQ29tb2RvIENBIExpbWl0ZWQxITAfBgNV
|
||||
BAMMGEFBQSBDZXJ0aWZpY2F0ZSBTZXJ2aWNlczAeFw0yNTA1MTQwMDAwMDBaFw0y
|
||||
ODEyMzEyMzU5NTlaMEAxCzAJBgNVBAYTAlVTMRQwEgYDVQQKDAtlTXVkaHJhIElu
|
||||
YzEbMBkGA1UEAwwSRU0gRFYgVExTIENBIC0gRzNDMHYwEAYHKoZIzj0CAQYFK4EE
|
||||
ACIDYgAEOnp4YsjETTJN3WkDOmeCwO/u/kBV5ooSP6A3G6O9kZRRTuFypDPjYw8C
|
||||
vRiAN9nlFJ+bd9uUooHfmHsv6oiLdL3+j9pMEJnubKGVZv4+8OX2sJpVbMyLpQVh
|
||||
3xsqRB0Bo4IBNTCCATEwHwYDVR0jBBgwFoAUoBEKIz6W8Qfs4q8p74Klf9AwpLQw
|
||||
HQYDVR0OBBYEFK5+EFw8KZbBqEcnBVnx4D3U5QrzMA4GA1UdDwEB/wQEAwIBBjAS
|
||||
BgNVHRMBAf8ECDAGAQH/AgEAMB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcD
|
||||
AjAxBgNVHSAEKjAoMAgGBmeBDAECATANBgsrBgEEAYOOIQEAATANBgsrBgEEAYOO
|
||||
IQECZDBDBgNVHR8EPDA6MDigNqA0hjJodHRwOi8vY3JsLmNvbW9kb2NhLmNvbS9B
|
||||
QUFDZXJ0aWZpY2F0ZVNlcnZpY2VzLmNybDA0BggrBgEFBQcBAQQoMCYwJAYIKwYB
|
||||
BQUHMAGGGGh0dHA6Ly9vY3NwLmNvbW9kb2NhLmNvbTANBgkqhkiG9w0BAQsFAAOC
|
||||
AQEAr7b6Mx11TIJgVzHt6EH5qGNWeiiqoMrp8E5/k8s+jvupx1n083pZ5XOzUzND
|
||||
wEUe0W4C4FIGUHNQ4XDwqOw0yK42xqm2dvCKgsko2UvDlT0haZVtTCeWMQOe5J/J
|
||||
ziaMfZwsZNfZ3aaI6XPtWDZtwY8AaNfyaeCpbtm5GcGnyNWk4jSNpbtpo6NjfXuP
|
||||
CFOm3rTjJ4zytP7Tso6iMm0L01cYFOlTmoPv4515R2r3SPnKDtijoB/D/vgxzojv
|
||||
DafgyG9afX8+laO9NbMMz4sqC1vZx3984i/sBNFiugm3Vvdq5OdJRjuGC1QILIs6
|
||||
dg/K3i8VQqRpbfaM9kIlMzZ3dQ==
|
||||
-----END CERTIFICATE-----
|
||||
@@ -0,0 +1,56 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIICODCCAb+gAwIBAgIUbWvtwSNK/Qro223vAqyz323+VBAwCgYIKoZIzj0EAwIw
|
||||
QDELMAkGA1UEBhMCVVMxFDASBgNVBAoMC2VNdWRocmEgSW5jMRswGQYDVQQDDBJF
|
||||
TSBEViBUTFMgQ0EgLSBHM0MwHhcNMjYxMDIwMDA1OTM2WhcNMjcwNTAzMDEwNDM2
|
||||
WjAvMS0wKwYDVQQDDCRkdi1nM2MtZW11ZGhyYS53ZS1oYXZlLWEtcHJvYmxlbS5j
|
||||
b20wWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAAScdBQpNUOOCcag6YEVFccNRSFe
|
||||
UIl02iH53vDfoWPJFS005WzqF3CPDclB/kygP2gWF4pg/r5gPYdnI3/6Vdzro4Gn
|
||||
MIGkMAwGA1UdEwEB/wQCMAAwDgYDVR0PAQH/BAQDAgeAMBMGA1UdJQQMMAoGCCsG
|
||||
AQUFBwMBMC8GA1UdEQQoMCaCJGR2LWczYy1lbXVkaHJhLndlLWhhdmUtYS1wcm9i
|
||||
bGVtLmNvbTAdBgNVHQ4EFgQUBj2yGO/TzDOE/u/6QeUvcW8y1iMwHwYDVR0jBBgw
|
||||
FoAUrn4QXDwplsGoRycFWfHgPdTlCvMwCgYIKoZIzj0EAwIDZwAwZAIwBQOOQqG9
|
||||
ccdzM/7XIlXLtota1esALaLmgEN/ZweosBIvk4xabHcERZInWVIwO45NAjBos8Ue
|
||||
NwDe1X3ahdK2lkxqBEoBMtUHJN18TnJKs2nK1gmXpMbmY5oKnAQQgCEZPA4=
|
||||
-----END CERTIFICATE-----
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIDaDCCAu6gAwIBAgIPWIsuPCu4zaE+GhIJJUMhMAoGCCqGSM49BAMDMFYxCzAJ
|
||||
BgNVBAYTAklOMSUwIwYDVQQKDBxlTXVkaHJhIFRlY2hub2xvZ2llcyBMaW1pdGVk
|
||||
MSAwHgYDVQQDDBdlbVNpZ24gUm9vdCBUTFMgQ0EgLSBHMzAeFw0yNTA0MzAxNTM1
|
||||
MDlaFw00MDA0MjgxODMwMDBaMEAxCzAJBgNVBAYTAlVTMRQwEgYDVQQKDAtlTXVk
|
||||
aHJhIEluYzEbMBkGA1UEAwwSRU0gRFYgVExTIENBIC0gRzNDMHYwEAYHKoZIzj0C
|
||||
AQYFK4EEACIDYgAEOnp4YsjETTJN3WkDOmeCwO/u/kBV5ooSP6A3G6O9kZRRTuFy
|
||||
pDPjYw8CvRiAN9nlFJ+bd9uUooHfmHsv6oiLdL3+j9pMEJnubKGVZv4+8OX2sJpV
|
||||
bMyLpQVh3xsqRB0Bo4IBljCCAZIwHwYDVR0jBBgwFoAU4TqrIXzx401UrtRyt/2L
|
||||
WUIgkFQwHQYDVR0OBBYEFK5+EFw8KZbBqEcnBVnx4D3U5QrzMB0GA1UdJQQWMBQG
|
||||
CCsGAQUFBwMBBggrBgEFBQcDAjBdBgNVHSAEVjBUMDkGCysGAQQBg44hAQABMCow
|
||||
KAYIKwYBBQUHAgEWHGh0dHA6Ly9yZXBvc2l0b3J5LmVtc2lnbi5jb20wDQYLKwYB
|
||||
BAGDjiEBAmQwCAYGZ4EMAQIBMHUGCCsGAQUFBwEBBGkwZzAkBggrBgEFBQcwAYYY
|
||||
aHR0cDovL29jc3AtYy5lbXNpZ24uY29tMD8GCCsGAQUFBzAChjNodHRwOi8vcmVw
|
||||
b3NpdG9yeS5lbXNpZ24uY29tL2NlcnRzL0VNRFZUTFNDQUczQy5wN2MwNwYDVR0f
|
||||
BDAwLjAsoCqgKIYmaHR0cDovL2NybC5lbXNpZ24uY29tLz9yb290dGxzY2FnMy5j
|
||||
cmwwDgYDVR0PAQH/BAQDAgEGMBIGA1UdEwEB/wQIMAYBAf8CAQAwCgYIKoZIzj0E
|
||||
AwMDaAAwZQIxAMEN2HWMocbuWdy1d33pH6JpMvP/V/DIS5PtZbv1Q7TXEbxrcyxg
|
||||
yuddOJscYDJJlAIwB4sH1BpbScDzkdD4tJBSwrG/1Mx82vb8YaySehNuu1+1oXNd
|
||||
AEKwaJ0mVzScy2+6
|
||||
-----END CERTIFICATE-----
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIDdDCCAvmgAwIBAgIQAJYaPvsTWVdu0EAFtGhKWzAKBggqhkjOPQQDAzBrMQsw
|
||||
CQYDVQQGEwJJTjETMBEGA1UECxMKZW1TaWduIFBLSTElMCMGA1UEChMcZU11ZGhy
|
||||
YSBUZWNobm9sb2dpZXMgTGltaXRlZDEgMB4GA1UEAxMXZW1TaWduIEVDQyBSb290
|
||||
IENBIC0gRzMwHhcNMjQwNzExMTU0MzM2WhcNMzkwNzA5MTgzMDAwWjBWMQswCQYD
|
||||
VQQGEwJJTjElMCMGA1UECgwcZU11ZGhyYSBUZWNobm9sb2dpZXMgTGltaXRlZDEg
|
||||
MB4GA1UEAwwXZW1TaWduIFJvb3QgVExTIENBIC0gRzMwdjAQBgcqhkjOPQIBBgUr
|
||||
gQQAIgNiAARfD9w/q1cWxL6D9bMGH6CQ4u+gAV6GN9s+PwQx9Bl4hC+0iU8QOk8m
|
||||
WO/RwXQy10DCSxT8vSIJClHKp+Ao4qJKdYeV0SwZ7LMQziyLfxIUOY6Grja/sQFw
|
||||
LKYf8EdDXAyjggF1MIIBcTAfBgNVHSMEGDAWgBR8XQKEE9TMipuBzhccLikenEhj
|
||||
QjAdBgNVHQ4EFgQU4TqrIXzx401UrtRyt/2LWUIgkFQwHQYDVR0lBBYwFAYIKwYB
|
||||
BQUHAwEGCCsGAQUFBwMCMD0GA1UdIAQ2MDQwMgYEVR0gADAqMCgGCCsGAQUFBwIB
|
||||
FhxodHRwOi8vcmVwb3NpdG9yeS5lbXNpZ24uY29tMHgGCCsGAQUFBwEBBGwwajAi
|
||||
BggrBgEFBQcwAYYWaHR0cDovL29jc3AuZW1zaWduLmNvbTBEBggrBgEFBQcwAoY4
|
||||
aHR0cDovL3JlcG9zaXRvcnkuZW1zaWduLmNvbS9jZXJ0cy9lbVNpZ25FQ0NSb290
|
||||
Q0FHMy5jcnQwMwYDVR0fBCwwKjAooCagJIYiaHR0cDovL2NybC5lbXNpZ24uY29t
|
||||
P1Jvb3RDQUczLmNybDAOBgNVHQ8BAf8EBAMCAQYwEgYDVR0TAQH/BAgwBgEB/wIB
|
||||
ATAKBggqhkjOPQQDAwNpADBmAjEAt9CIpRGp46QHQYDUBnLKw+vvK0/h9vmilEAg
|
||||
TroUl/anH4x2rXHqiCtBTBfgKDegAjEA8WpG1LTZdAIL0FSYqxXeREaeTSxEofM0
|
||||
1jJH3jGvCXC79LU9UPJ2o3TU+UIj2gMV
|
||||
-----END CERTIFICATE-----
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIICQDCCAcagAwIBAgIUUYu3O8bpN1GPvCIHqWpT/FYUNDowCgYIKoZIzj0EAwIw
|
||||
PzELMAkGA1UEBhMCTkwxEzARBgNVBAoMCmVNdWRocmEgQlYxGzAZBgNVBAMMEkVN
|
||||
IEVWIFRMUyBDQSAtIEczQjAeFw0yNjEwMjAwMjIyNDFaFw0yNzA1MDMwMjI3NDFa
|
||||
MDMxMTAvBgNVBAMMKGV2LWczYi1hYWEtZW11ZGhyYS53ZS1oYXZlLWEtcHJvYmxl
|
||||
bS5jb20wWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAASjDLpnVdmvXM6R3UB2o42a
|
||||
1GnrYfxRXo7UII36mBl9mIS7p9MkAryhPdxitFHkgTrrbypVtzpydNtJIGGGXUwE
|
||||
o4GrMIGoMAwGA1UdEwEB/wQCMAAwDgYDVR0PAQH/BAQDAgeAMBMGA1UdJQQMMAoG
|
||||
CCsGAQUFBwMBMDMGA1UdEQQsMCqCKGV2LWczYi1hYWEtZW11ZGhyYS53ZS1oYXZl
|
||||
LWEtcHJvYmxlbS5jb20wHQYDVR0OBBYEFEVBY1ecTN9bLMDl4ifdnN2WZL+WMB8G
|
||||
A1UdIwQYMBaAFD7cINmJRpiHe4c/2IFHslwV2EcGMAoGCCqGSM49BAMCA2gAMGUC
|
||||
MQCyvr9gktgmtsuTuFleFV+YExNNKLf3GZieuUprQcERrzh798oYKavTqZeYf9Kz
|
||||
RhECMCVs1B7KPh7/lgHRly14Z8RJA8ApR7e2cRREW5uPKFOSDlZ/RIezxsfEzsQ4
|
||||
lAODeA==
|
||||
-----END CERTIFICATE-----
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIDzTCCArWgAwIBAgIRANj4aF5PK9nYJAKoWhwetzEwDQYJKoZIhvcNAQELBQAw
|
||||
ezELMAkGA1UEBhMCR0IxGzAZBgNVBAgMEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4G
|
||||
A1UEBwwHU2FsZm9yZDEaMBgGA1UECgwRQ29tb2RvIENBIExpbWl0ZWQxITAfBgNV
|
||||
BAMMGEFBQSBDZXJ0aWZpY2F0ZSBTZXJ2aWNlczAeFw0yNTA1MTQwMDAwMDBaFw0y
|
||||
ODEyMzEyMzU5NTlaMD8xCzAJBgNVBAYTAk5MMRMwEQYDVQQKDAplTXVkaHJhIEJW
|
||||
MRswGQYDVQQDDBJFTSBFViBUTFMgQ0EgLSBHM0IwdjAQBgcqhkjOPQIBBgUrgQQA
|
||||
IgNiAAQl26haZNOX2gsQUunqVG5Ie+/qnJ2daKDF6tz+73Wmm7XW/AH0K/vKbrQL
|
||||
zWTAdn42/9Bdl6gTxaoMbJUw55z18IZkaPE4BkIKpchRhAseJSVA55DktlTDhS7W
|
||||
SvHoZX2jggE0MIIBMDAfBgNVHSMEGDAWgBSgEQojPpbxB+zirynvgqV/0DCktDAd
|
||||
BgNVHQ4EFgQUPtwg2YlGmId7hz/YgUeyXBXYRwYwDgYDVR0PAQH/BAQDAgEGMBIG
|
||||
A1UdEwEB/wQIMAYBAf8CAQAwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMC
|
||||
MDAGA1UdIAQpMCcwBwYFZ4EMAQEwDQYLKwYBBAGDjiEBAAEwDQYLKwYBBAGDjiEB
|
||||
AngwQwYDVR0fBDwwOjA4oDagNIYyaHR0cDovL2NybC5jb21vZG9jYS5jb20vQUFB
|
||||
Q2VydGlmaWNhdGVTZXJ2aWNlcy5jcmwwNAYIKwYBBQUHAQEEKDAmMCQGCCsGAQUF
|
||||
BzABhhhodHRwOi8vb2NzcC5jb21vZG9jYS5jb20wDQYJKoZIhvcNAQELBQADggEB
|
||||
AJNFuAFW9Gnw/8+kCO4KIlZdnM1Sh2u+8cK0EYAHhXpb5FlGvYkcARigo5oS7VoM
|
||||
7FOYlTueV6yICZKNAYawZPJPhNCGK+rp3cQBEz6d0n8OZEYZov9lb3ll8ORgSDuf
|
||||
FVIW6Gagg/UnOamLBTOR8eD5V73SCme/B2cHUDw+m5E8cc+0Ujzz3Cn3ex5JrhXk
|
||||
2T3s8PoVzEmhTgGboFfcWO1EYhPPrYwdxPYRQIXAXUXhLEOVNL9ASbZJLFI8E1IM
|
||||
m7NwxVN2s38jWpaCUaaF0tg7hfiy7oe13rzJfHsdqldJVTTUTPWVx8TC4ju0VLet
|
||||
bAlwhwFT6AP8uIVMGUHNw24=
|
||||
-----END CERTIFICATE-----
|
||||
@@ -0,0 +1,56 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIICNzCCAb6gAwIBAgIUPfthO0PYGAYKs9lQ4gp1IOkOr2MwCgYIKoZIzj0EAwIw
|
||||
PzELMAkGA1UEBhMCTkwxEzARBgNVBAoMCmVNdWRocmEgQlYxGzAZBgNVBAMMEkVN
|
||||
IEVWIFRMUyBDQSAtIEczQjAeFw0yNjEwMjAwMDU5MzZaFw0yNzA1MDMwMTA0MzZa
|
||||
MC8xLTArBgNVBAMMJGV2LWczYi1lbXVkaHJhLndlLWhhdmUtYS1wcm9ibGVtLmNv
|
||||
bTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABPZPwlA3io/AX/pMwu0dNl9WMxrr
|
||||
C9j/xMLHgG72cWVzoX62REmvp9zyXQ04kHP9/sW6KnCcm5ZZ5i4YoJbDPASjgacw
|
||||
gaQwDAYDVR0TAQH/BAIwADAOBgNVHQ8BAf8EBAMCB4AwEwYDVR0lBAwwCgYIKwYB
|
||||
BQUHAwEwLwYDVR0RBCgwJoIkZXYtZzNiLWVtdWRocmEud2UtaGF2ZS1hLXByb2Js
|
||||
ZW0uY29tMB0GA1UdDgQWBBSvX4QR4ERW5uNUaMq0yYzXCZf0ZjAfBgNVHSMEGDAW
|
||||
gBQ+3CDZiUaYh3uHP9iBR7JcFdhHBjAKBggqhkjOPQQDAgNnADBkAjACy/uJR01M
|
||||
/O9IEZyRHX1LSU28bmg542K0IX9jDRTrZXUqkDWfiZIL7xfnrRrBpHoCMFbBIaOa
|
||||
WwWtFtDg9ZqQvktsWS950u5qfW1sChgWNya/E3IMKFgtoHm2xrZsmfwLSQ==
|
||||
-----END CERTIFICATE-----
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIDaDCCAu2gAwIBAgIQAMfEHv6S2CXwXq6EGkqCwTAKBggqhkjOPQQDAzBWMQsw
|
||||
CQYDVQQGEwJJTjElMCMGA1UECgwcZU11ZGhyYSBUZWNobm9sb2dpZXMgTGltaXRl
|
||||
ZDEgMB4GA1UEAwwXZW1TaWduIFJvb3QgVExTIENBIC0gRzMwHhcNMjUwNDMwMTYw
|
||||
MzMxWhcNNDAwNDI4MTgzMDAwWjA/MQswCQYDVQQGEwJOTDETMBEGA1UECgwKZU11
|
||||
ZGhyYSBCVjEbMBkGA1UEAwwSRU0gRVYgVExTIENBIC0gRzNCMHYwEAYHKoZIzj0C
|
||||
AQYFK4EEACIDYgAEJduoWmTTl9oLEFLp6lRuSHvv6pydnWigxerc/u91ppu11vwB
|
||||
9Cv7ym60C81kwHZ+Nv/QXZeoE8WqDGyVMOec9fCGZGjxOAZCCqXIUYQLHiUlQOeQ
|
||||
5LZUw4Uu1krx6GV9o4IBlTCCAZEwHwYDVR0jBBgwFoAU4TqrIXzx401UrtRyt/2L
|
||||
WUIgkFQwHQYDVR0OBBYEFD7cINmJRpiHe4c/2IFHslwV2EcGMB0GA1UdJQQWMBQG
|
||||
CCsGAQUFBwMBBggrBgEFBQcDAjBcBgNVHSAEVTBTMDkGCysGAQQBg44hAQABMCow
|
||||
KAYIKwYBBQUHAgEWHGh0dHA6Ly9yZXBvc2l0b3J5LmVtc2lnbi5jb20wDQYLKwYB
|
||||
BAGDjiEBAngwBwYFZ4EMAQEwdQYIKwYBBQUHAQEEaTBnMCQGCCsGAQUFBzABhhho
|
||||
dHRwOi8vb2NzcC1iLmVtc2lnbi5jb20wPwYIKwYBBQUHMAKGM2h0dHA6Ly9yZXBv
|
||||
c2l0b3J5LmVtc2lnbi5jb20vY2VydHMvRU1FVlRMU0NBRzNCLnA3YzA3BgNVHR8E
|
||||
MDAuMCygKqAohiZodHRwOi8vY3JsLmVtc2lnbi5jb20vP3Jvb3R0bHNjYWczLmNy
|
||||
bDAOBgNVHQ8BAf8EBAMCAQYwEgYDVR0TAQH/BAgwBgEB/wIBADAKBggqhkjOPQQD
|
||||
AwNpADBmAjEA9Jq999oaOUKhFwopw33iCmt0OHIn3gjT43GfuOHR/YckXdwoNdDA
|
||||
lIEhz0NgRO3/AjEAvY/a3OMvktIZLYjn+WLKmp0QhrIMV+e6EpS7j4GVeOKouNp6
|
||||
MN/P+ByGeiRMghzB
|
||||
-----END CERTIFICATE-----
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIDdDCCAvmgAwIBAgIQAJYaPvsTWVdu0EAFtGhKWzAKBggqhkjOPQQDAzBrMQsw
|
||||
CQYDVQQGEwJJTjETMBEGA1UECxMKZW1TaWduIFBLSTElMCMGA1UEChMcZU11ZGhy
|
||||
YSBUZWNobm9sb2dpZXMgTGltaXRlZDEgMB4GA1UEAxMXZW1TaWduIEVDQyBSb290
|
||||
IENBIC0gRzMwHhcNMjQwNzExMTU0MzM2WhcNMzkwNzA5MTgzMDAwWjBWMQswCQYD
|
||||
VQQGEwJJTjElMCMGA1UECgwcZU11ZGhyYSBUZWNobm9sb2dpZXMgTGltaXRlZDEg
|
||||
MB4GA1UEAwwXZW1TaWduIFJvb3QgVExTIENBIC0gRzMwdjAQBgcqhkjOPQIBBgUr
|
||||
gQQAIgNiAARfD9w/q1cWxL6D9bMGH6CQ4u+gAV6GN9s+PwQx9Bl4hC+0iU8QOk8m
|
||||
WO/RwXQy10DCSxT8vSIJClHKp+Ao4qJKdYeV0SwZ7LMQziyLfxIUOY6Grja/sQFw
|
||||
LKYf8EdDXAyjggF1MIIBcTAfBgNVHSMEGDAWgBR8XQKEE9TMipuBzhccLikenEhj
|
||||
QjAdBgNVHQ4EFgQU4TqrIXzx401UrtRyt/2LWUIgkFQwHQYDVR0lBBYwFAYIKwYB
|
||||
BQUHAwEGCCsGAQUFBwMCMD0GA1UdIAQ2MDQwMgYEVR0gADAqMCgGCCsGAQUFBwIB
|
||||
FhxodHRwOi8vcmVwb3NpdG9yeS5lbXNpZ24uY29tMHgGCCsGAQUFBwEBBGwwajAi
|
||||
BggrBgEFBQcwAYYWaHR0cDovL29jc3AuZW1zaWduLmNvbTBEBggrBgEFBQcwAoY4
|
||||
aHR0cDovL3JlcG9zaXRvcnkuZW1zaWduLmNvbS9jZXJ0cy9lbVNpZ25FQ0NSb290
|
||||
Q0FHMy5jcnQwMwYDVR0fBCwwKjAooCagJIYiaHR0cDovL2NybC5lbXNpZ24uY29t
|
||||
P1Jvb3RDQUczLmNybDAOBgNVHQ8BAf8EBAMCAQYwEgYDVR0TAQH/BAgwBgEB/wIB
|
||||
ATAKBggqhkjOPQQDAwNpADBmAjEAt9CIpRGp46QHQYDUBnLKw+vvK0/h9vmilEAg
|
||||
TroUl/anH4x2rXHqiCtBTBfgKDegAjEA8WpG1LTZdAIL0FSYqxXeREaeTSxEofM0
|
||||
1jJH3jGvCXC79LU9UPJ2o3TU+UIj2gMV
|
||||
-----END CERTIFICATE-----
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIICQDCCAcegAwIBAgIUTMUrGzSU+0BySFdrN3ALZq9Spv8wCgYIKoZIzj0EAwIw
|
||||
QDELMAkGA1UEBhMCVVMxFDASBgNVBAoMC2VNdWRocmEgSW5jMRswGQYDVQQDDBJF
|
||||
TSBFViBUTFMgQ0EgLSBHM0MwHhcNMjYxMDIwMDIyMjQyWhcNMjcwNTAzMDIyNzQy
|
||||
WjAzMTEwLwYDVQQDDChldi1nM2MtYWFhLWVtdWRocmEud2UtaGF2ZS1hLXByb2Js
|
||||
ZW0uY29tMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEglClDbotLFFeY9SpryT4
|
||||
k958tvriY2DDnaPsmjQaecRhB6+L/8hLw6FMt+HT8SLvjIZnFY+AH92cBjedcNST
|
||||
AKOBqzCBqDAMBgNVHRMBAf8EAjAAMA4GA1UdDwEB/wQEAwIHgDATBgNVHSUEDDAK
|
||||
BggrBgEFBQcDATAzBgNVHREELDAqgihldi1nM2MtYWFhLWVtdWRocmEud2UtaGF2
|
||||
ZS1hLXByb2JsZW0uY29tMB0GA1UdDgQWBBSVtGbn6xUT1iosUeaGA15Nq640aTAf
|
||||
BgNVHSMEGDAWgBSmJV6AqU2385Z8G8eR35K87kvrXzAKBggqhkjOPQQDAgNnADBk
|
||||
AjAg7b5bIK9X50QEX0U6G8fQh12BnFQ079Iluzd0Y2TJWCjtVXZi3nOciM8RzNKM
|
||||
tZECMCBf0JOyMt8RNS93Wj8kYbWkCtoKoUrqx7G3JTfGReKci+DdTvkF8/3F6doi
|
||||
9KB22Q==
|
||||
-----END CERTIFICATE-----
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIDzTCCArWgAwIBAgIQBfB4dS3s56G7hZXTAxZxcTANBgkqhkiG9w0BAQsFADB7
|
||||
MQswCQYDVQQGEwJHQjEbMBkGA1UECAwSR3JlYXRlciBNYW5jaGVzdGVyMRAwDgYD
|
||||
VQQHDAdTYWxmb3JkMRowGAYDVQQKDBFDb21vZG8gQ0EgTGltaXRlZDEhMB8GA1UE
|
||||
AwwYQUFBIENlcnRpZmljYXRlIFNlcnZpY2VzMB4XDTI1MDUxNDAwMDAwMFoXDTI4
|
||||
MTIzMTIzNTk1OVowQDELMAkGA1UEBhMCVVMxFDASBgNVBAoMC2VNdWRocmEgSW5j
|
||||
MRswGQYDVQQDDBJFTSBFViBUTFMgQ0EgLSBHM0MwdjAQBgcqhkjOPQIBBgUrgQQA
|
||||
IgNiAAQtVugppvuCxkpGl9lgr5UekGeukfkm00MG2kioqut4abamfm0nQm4EQqoD
|
||||
s24ZiN/2NlFO4c8H9bpLP/KgZkWufTHVhmrTGqPSRusLMXfumct1ZsUWznSAimaP
|
||||
kfDEf8qjggE0MIIBMDAfBgNVHSMEGDAWgBSgEQojPpbxB+zirynvgqV/0DCktDAd
|
||||
BgNVHQ4EFgQUpiVegKlNt/OWfBvHkd+SvO5L618wDgYDVR0PAQH/BAQDAgEGMBIG
|
||||
A1UdEwEB/wQIMAYBAf8CAQAwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMC
|
||||
MDAGA1UdIAQpMCcwBwYFZ4EMAQEwDQYLKwYBBAGDjiEBAAEwDQYLKwYBBAGDjiEB
|
||||
AngwQwYDVR0fBDwwOjA4oDagNIYyaHR0cDovL2NybC5jb21vZG9jYS5jb20vQUFB
|
||||
Q2VydGlmaWNhdGVTZXJ2aWNlcy5jcmwwNAYIKwYBBQUHAQEEKDAmMCQGCCsGAQUF
|
||||
BzABhhhodHRwOi8vb2NzcC5jb21vZG9jYS5jb20wDQYJKoZIhvcNAQELBQADggEB
|
||||
AIFfHiaMSjnrdZygRYV4ekRBmG7ybcFk4hGQWMMyMFi0cAEk2+dGfz5bl8IaIbLq
|
||||
z+uVJzhOgVEX7WqIfgbWfFsfPYHYjPHBmy7SlZ3BwA86IQ6qF/etpX2/qL9A5yRN
|
||||
N3nVowUpaqV6H3aCpkAh3wR2ZNkvAbLDvPz3J0l+sEWjWv+ZISOV4R+hXDTdPXV0
|
||||
1lKxnbwqEsdo/sE2QkCDKwFrZh/0uKjWspydJj+jSaDLTrGNx50Dgmn7VEcD1ZI5
|
||||
FXjknVUiO25YlmS1dtVZSBbyUKSjlrP/yDwPcZunoAsmCAAtCKsFLviLaymzvsKI
|
||||
eSSDGxpSySp2VCxompGDk8g=
|
||||
-----END CERTIFICATE-----
|
||||
@@ -0,0 +1,56 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIICOTCCAb+gAwIBAgIUOcw5N4XGLiB/3GvB05bJuGoZPR0wCgYIKoZIzj0EAwIw
|
||||
QDELMAkGA1UEBhMCVVMxFDASBgNVBAoMC2VNdWRocmEgSW5jMRswGQYDVQQDDBJF
|
||||
TSBFViBUTFMgQ0EgLSBHM0MwHhcNMjYxMDIwMDA1OTM3WhcNMjcwNTAzMDEwNDM3
|
||||
WjAvMS0wKwYDVQQDDCRldi1nM2MtZW11ZGhyYS53ZS1oYXZlLWEtcHJvYmxlbS5j
|
||||
b20wWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAAQo7U02MAUO8EEMomXwIl+kSQzm
|
||||
b+nvXKIZ/hC4rZ1GpXchv4SzTrV1SB/Qg+pD6OBEXbBBnPmQNkePNQTlujqRo4Gn
|
||||
MIGkMAwGA1UdEwEB/wQCMAAwDgYDVR0PAQH/BAQDAgeAMBMGA1UdJQQMMAoGCCsG
|
||||
AQUFBwMBMC8GA1UdEQQoMCaCJGV2LWczYy1lbXVkaHJhLndlLWhhdmUtYS1wcm9i
|
||||
bGVtLmNvbTAdBgNVHQ4EFgQUqxe+7qS+MxNyioJZhQsL5MEEw6swHwYDVR0jBBgw
|
||||
FoAUpiVegKlNt/OWfBvHkd+SvO5L618wCgYIKoZIzj0EAwIDaAAwZQIwC/CxSIov
|
||||
wcar3sV98B8cL+QuR2kkJdLdXsFKzg/HDSJ1MOjqTGm924LB6r9nl0f7AjEAzcwr
|
||||
WQOYY9eycqIz+LG2daYS6LOctj+CjoAk/CLugWPxGxp4Aerfau2P1z1h5jAA
|
||||
-----END CERTIFICATE-----
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIDaDCCAu6gAwIBAgIQAIAJkopkEHYBrwX6LxnxCTAKBggqhkjOPQQDAzBWMQsw
|
||||
CQYDVQQGEwJJTjElMCMGA1UECgwcZU11ZGhyYSBUZWNobm9sb2dpZXMgTGltaXRl
|
||||
ZDEgMB4GA1UEAwwXZW1TaWduIFJvb3QgVExTIENBIC0gRzMwHhcNMjUwNDMwMTUz
|
||||
OTMxWhcNNDAwNDI4MTgzMDAwWjBAMQswCQYDVQQGEwJVUzEUMBIGA1UECgwLZU11
|
||||
ZGhyYSBJbmMxGzAZBgNVBAMMEkVNIEVWIFRMUyBDQSAtIEczQzB2MBAGByqGSM49
|
||||
AgEGBSuBBAAiA2IABC1W6Cmm+4LGSkaX2WCvlR6QZ66R+SbTQwbaSKiq63hptqZ+
|
||||
bSdCbgRCqgOzbhmI3/Y2UU7hzwf1uks/8qBmRa59MdWGatMao9JG6wsxd+6Zy3Vm
|
||||
xRbOdICKZo+R8MR/yqOCAZUwggGRMB8GA1UdIwQYMBaAFOE6qyF88eNNVK7Ucrf9
|
||||
i1lCIJBUMB0GA1UdDgQWBBSmJV6AqU2385Z8G8eR35K87kvrXzAdBgNVHSUEFjAU
|
||||
BggrBgEFBQcDAQYIKwYBBQUHAwIwXAYDVR0gBFUwUzA5BgsrBgEEAYOOIQEAATAq
|
||||
MCgGCCsGAQUFBwIBFhxodHRwOi8vcmVwb3NpdG9yeS5lbXNpZ24uY29tMA0GCysG
|
||||
AQQBg44hAQJ4MAcGBWeBDAEBMHUGCCsGAQUFBwEBBGkwZzAkBggrBgEFBQcwAYYY
|
||||
aHR0cDovL29jc3AtYy5lbXNpZ24uY29tMD8GCCsGAQUFBzAChjNodHRwOi8vcmVw
|
||||
b3NpdG9yeS5lbXNpZ24uY29tL2NlcnRzL0VNRVZUTFNDQUczQy5wN2MwNwYDVR0f
|
||||
BDAwLjAsoCqgKIYmaHR0cDovL2NybC5lbXNpZ24uY29tLz9yb290dGxzY2FnMy5j
|
||||
cmwwDgYDVR0PAQH/BAQDAgEGMBIGA1UdEwEB/wQIMAYBAf8CAQAwCgYIKoZIzj0E
|
||||
AwMDaAAwZQIwBy1wTnzH4lXDTy2PD91ysLjvnlVN3JbJmc1BIMEU8TOLkYVHibMc
|
||||
9HeyonikFSfmAjEA6uE3QRxlrZTz/1/lXPTEMUhoauAtycagtlNFp79kjhvNa2/f
|
||||
Aal1GNxsOutLnr0Y
|
||||
-----END CERTIFICATE-----
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIDdDCCAvmgAwIBAgIQAJYaPvsTWVdu0EAFtGhKWzAKBggqhkjOPQQDAzBrMQsw
|
||||
CQYDVQQGEwJJTjETMBEGA1UECxMKZW1TaWduIFBLSTElMCMGA1UEChMcZU11ZGhy
|
||||
YSBUZWNobm9sb2dpZXMgTGltaXRlZDEgMB4GA1UEAxMXZW1TaWduIEVDQyBSb290
|
||||
IENBIC0gRzMwHhcNMjQwNzExMTU0MzM2WhcNMzkwNzA5MTgzMDAwWjBWMQswCQYD
|
||||
VQQGEwJJTjElMCMGA1UECgwcZU11ZGhyYSBUZWNobm9sb2dpZXMgTGltaXRlZDEg
|
||||
MB4GA1UEAwwXZW1TaWduIFJvb3QgVExTIENBIC0gRzMwdjAQBgcqhkjOPQIBBgUr
|
||||
gQQAIgNiAARfD9w/q1cWxL6D9bMGH6CQ4u+gAV6GN9s+PwQx9Bl4hC+0iU8QOk8m
|
||||
WO/RwXQy10DCSxT8vSIJClHKp+Ao4qJKdYeV0SwZ7LMQziyLfxIUOY6Grja/sQFw
|
||||
LKYf8EdDXAyjggF1MIIBcTAfBgNVHSMEGDAWgBR8XQKEE9TMipuBzhccLikenEhj
|
||||
QjAdBgNVHQ4EFgQU4TqrIXzx401UrtRyt/2LWUIgkFQwHQYDVR0lBBYwFAYIKwYB
|
||||
BQUHAwEGCCsGAQUFBwMCMD0GA1UdIAQ2MDQwMgYEVR0gADAqMCgGCCsGAQUFBwIB
|
||||
FhxodHRwOi8vcmVwb3NpdG9yeS5lbXNpZ24uY29tMHgGCCsGAQUFBwEBBGwwajAi
|
||||
BggrBgEFBQcwAYYWaHR0cDovL29jc3AuZW1zaWduLmNvbTBEBggrBgEFBQcwAoY4
|
||||
aHR0cDovL3JlcG9zaXRvcnkuZW1zaWduLmNvbS9jZXJ0cy9lbVNpZ25FQ0NSb290
|
||||
Q0FHMy5jcnQwMwYDVR0fBCwwKjAooCagJIYiaHR0cDovL2NybC5lbXNpZ24uY29t
|
||||
P1Jvb3RDQUczLmNybDAOBgNVHQ8BAf8EBAMCAQYwEgYDVR0TAQH/BAgwBgEB/wIB
|
||||
ATAKBggqhkjOPQQDAwNpADBmAjEAt9CIpRGp46QHQYDUBnLKw+vvK0/h9vmilEAg
|
||||
TroUl/anH4x2rXHqiCtBTBfgKDegAjEA8WpG1LTZdAIL0FSYqxXeREaeTSxEofM0
|
||||
1jJH3jGvCXC79LU9UPJ2o3TU+UIj2gMV
|
||||
-----END CERTIFICATE-----
|
||||
@@ -0,0 +1,466 @@
|
||||
[
|
||||
{
|
||||
"name": "EM DV TLS CA - G3B",
|
||||
"usage": "tls",
|
||||
"issued_by": "emSign Root TLS CA - G3",
|
||||
"issuer_operator": "eMudhra",
|
||||
"issuer_sha256": "7DD78D5F4F13459A83DFF9ABBBA62EDBAF6F2D102BF257FD712F4D9F2746ED8D",
|
||||
"subject": "CN=EM DV TLS CA - G3B,O=eMudhra BV,C=NL",
|
||||
"issuer": "CN=emSign Root TLS CA - G3,O=eMudhra Technologies Limited,C=IN",
|
||||
"serial_hex": "A4AEE678F23362B4C0E460B772F214",
|
||||
"sha256": "42C4ACCD12C4F59597925E1A08A7A90102608543809308D50EC427E118BA1493",
|
||||
"spki_sha256": "EBED6475C2C2D80FCB199F3FE8DD6E6B3CE60B77EB15D4E42C3DEC0C6F5B5105",
|
||||
"subject_key_identifier": "E21E2FE71C19390E554B62C120245735248A2514",
|
||||
"authority_key_identifier": "E13AAB217CF1E34D54AED472B7FD8B5942209054",
|
||||
"key": {
|
||||
"algorithm": "EC",
|
||||
"curve": "secp384r1",
|
||||
"bits": 384
|
||||
},
|
||||
"signature_algorithm": "ecdsa-with-SHA384",
|
||||
"not_before": "2025-04-30T15:54:08Z",
|
||||
"not_after": "2040-04-28T18:30:00Z",
|
||||
"revocation": {
|
||||
"status": "revoked",
|
||||
"date": "2026-10-18T09:24:32Z",
|
||||
"reason": "ca_compromise",
|
||||
"crl_url": "http://crl.emsign.com/?roottlscag3.crl"
|
||||
},
|
||||
"pem": "-----BEGIN CERTIFICATE-----\nMIIDaDCCAu6gAwIBAgIQAKSu5njyM2K0wORgt3LyFDAKBggqhkjOPQQDAzBWMQsw\nCQYDVQQGEwJJTjElMCMGA1UECgwcZU11ZGhyYSBUZWNobm9sb2dpZXMgTGltaXRl\nZDEgMB4GA1UEAwwXZW1TaWduIFJvb3QgVExTIENBIC0gRzMwHhcNMjUwNDMwMTU1\nNDA4WhcNNDAwNDI4MTgzMDAwWjA/MQswCQYDVQQGEwJOTDETMBEGA1UECgwKZU11\nZGhyYSBCVjEbMBkGA1UEAwwSRU0gRFYgVExTIENBIC0gRzNCMHYwEAYHKoZIzj0C\nAQYFK4EEACIDYgAE557ZwEqXc8RzXlERHahawr1eZKs3S4DhLLwgOOweTw47gS/J\nhZ4eZ6+wTi+LyX90hbplqpEe6KTkW2LVzAPVa+Z4s8OMtNyTfKJGDiOEU8iSk10p\n1E+R4VYGtjzO2rRoo4IBljCCAZIwHwYDVR0jBBgwFoAU4TqrIXzx401UrtRyt/2L\nWUIgkFQwHQYDVR0OBBYEFOIeL+ccGTkOVUtiwSAkVzUkiiUUMB0GA1UdJQQWMBQG\nCCsGAQUFBwMBBggrBgEFBQcDAjBdBgNVHSAEVjBUMDkGCysGAQQBg44hAQABMCow\nKAYIKwYBBQUHAgEWHGh0dHA6Ly9yZXBvc2l0b3J5LmVtc2lnbi5jb20wDQYLKwYB\nBAGDjiEBAmQwCAYGZ4EMAQIBMHUGCCsGAQUFBwEBBGkwZzAkBggrBgEFBQcwAYYY\naHR0cDovL29jc3AtYi5lbXNpZ24uY29tMD8GCCsGAQUFBzAChjNodHRwOi8vcmVw\nb3NpdG9yeS5lbXNpZ24uY29tL2NlcnRzL0VNRFZUTFNDQUczQi5wN2MwNwYDVR0f\nBDAwLjAsoCqgKIYmaHR0cDovL2NybC5lbXNpZ24uY29tLz9yb290dGxzY2FnMy5j\ncmwwDgYDVR0PAQH/BAQDAgEGMBIGA1UdEwEB/wQIMAYBAf8CAQAwCgYIKoZIzj0E\nAwMDaAAwZQIwLmsGXi6oFMJ2klkcEMXMug6gvxQWZWkewiDWVzQyl2L6z6DuQfPO\nIbh1grvdULDCAjEAxZvLlxUavG69J6wnSjC8XBbqNIQsJ6CE2QdNOe84Ptxb6VG8\ngt2TlfJbHii+KNvK\n-----END CERTIFICATE-----"
|
||||
},
|
||||
{
|
||||
"name": "EM DV TLS CA - G3B",
|
||||
"usage": "tls",
|
||||
"issued_by": "AAA Certificate Services",
|
||||
"issuer_operator": "Sectigo",
|
||||
"issuer_sha256": "D7A7A0FB5D7E2731D771E9484EBCDEF71D5F0C3E0A2948782BC83EE0EA699EF4",
|
||||
"subject": "CN=EM DV TLS CA - G3B,O=eMudhra BV,C=NL",
|
||||
"issuer": "CN=AAA Certificate Services,O=Comodo CA Limited,L=Salford,ST=Greater Manchester,C=GB",
|
||||
"serial_hex": "A024FDF4BA665C785CE4A020483858B7",
|
||||
"sha256": "45A883F349AEDFD5F3A649B5498FFB121712BA8B448F3F2A91DBC5A766FCDDBE",
|
||||
"spki_sha256": "EBED6475C2C2D80FCB199F3FE8DD6E6B3CE60B77EB15D4E42C3DEC0C6F5B5105",
|
||||
"subject_key_identifier": "E21E2FE71C19390E554B62C120245735248A2514",
|
||||
"authority_key_identifier": "A0110A233E96F107ECE2AF29EF82A57FD030A4B4",
|
||||
"key": {
|
||||
"algorithm": "EC",
|
||||
"curve": "secp384r1",
|
||||
"bits": 384
|
||||
},
|
||||
"signature_algorithm": "sha256WithRSAEncryption",
|
||||
"not_before": "2025-05-14T00:00:00Z",
|
||||
"not_after": "2028-12-31T23:59:59Z",
|
||||
"revocation": {
|
||||
"status": "revoked",
|
||||
"date": "2026-10-19T10:35:36Z",
|
||||
"reason": "ca_compromise",
|
||||
"crl_url": "http://crl.comodoca.com/AAACertificateServices.crl"
|
||||
},
|
||||
"pem": "-----BEGIN CERTIFICATE-----\nMIIDzjCCAragAwIBAgIRAKAk/fS6Zlx4XOSgIEg4WLcwDQYJKoZIhvcNAQELBQAw\nezELMAkGA1UEBhMCR0IxGzAZBgNVBAgMEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4G\nA1UEBwwHU2FsZm9yZDEaMBgGA1UECgwRQ29tb2RvIENBIExpbWl0ZWQxITAfBgNV\nBAMMGEFBQSBDZXJ0aWZpY2F0ZSBTZXJ2aWNlczAeFw0yNTA1MTQwMDAwMDBaFw0y\nODEyMzEyMzU5NTlaMD8xCzAJBgNVBAYTAk5MMRMwEQYDVQQKDAplTXVkaHJhIEJW\nMRswGQYDVQQDDBJFTSBEViBUTFMgQ0EgLSBHM0IwdjAQBgcqhkjOPQIBBgUrgQQA\nIgNiAATnntnASpdzxHNeUREdqFrCvV5kqzdLgOEsvCA47B5PDjuBL8mFnh5nr7BO\nL4vJf3SFumWqkR7opORbYtXMA9Vr5nizw4y03JN8okYOI4RTyJKTXSnUT5HhVga2\nPM7atGijggE1MIIBMTAfBgNVHSMEGDAWgBSgEQojPpbxB+zirynvgqV/0DCktDAd\nBgNVHQ4EFgQU4h4v5xwZOQ5VS2LBICRXNSSKJRQwDgYDVR0PAQH/BAQDAgEGMBIG\nA1UdEwEB/wQIMAYBAf8CAQAwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMC\nMDEGA1UdIAQqMCgwCAYGZ4EMAQIBMA0GCysGAQQBg44hAQABMA0GCysGAQQBg44h\nAQJkMEMGA1UdHwQ8MDowOKA2oDSGMmh0dHA6Ly9jcmwuY29tb2RvY2EuY29tL0FB\nQUNlcnRpZmljYXRlU2VydmljZXMuY3JsMDQGCCsGAQUFBwEBBCgwJjAkBggrBgEF\nBQcwAYYYaHR0cDovL29jc3AuY29tb2RvY2EuY29tMA0GCSqGSIb3DQEBCwUAA4IB\nAQBzQ1IoqpdotM6gFrR4Q3YrKifOLeaj5XXunldpVE/vZn2TxC2y0s8oYiLR5ICE\nwqVXSB7N6BM1Mp+Nif7Jbhb2IswAfSR4Iik8+B0iRmi7lYoNlycCgHjmsjOmZaxM\nQkPmjXAJbG5f4V41OsvJZmS7lI3r0IuvEDVX3GAZ13on6IImSs9ybNpHQ7NCqkMQ\n7wH4uAO1dtpFycgJSdHaCOhHjrox2BTYGLdpUrYwY3EOAjlZ8UGgsxs7mG+xquMl\nbN1LFURwfnkVMP3T/DBIZ/ys2+lNFyxK3FlArrV4/000uTnVB1lO8oGORYdXoT5V\nGUyRCEIDhF5TBUuKF4E3kf1h\n-----END CERTIFICATE-----"
|
||||
},
|
||||
{
|
||||
"name": "EM OV TLS CA - G3B",
|
||||
"usage": "tls",
|
||||
"issued_by": "emSign Root TLS CA - G3",
|
||||
"issuer_operator": "eMudhra",
|
||||
"issuer_sha256": "7DD78D5F4F13459A83DFF9ABBBA62EDBAF6F2D102BF257FD712F4D9F2746ED8D",
|
||||
"subject": "CN=EM OV TLS CA - G3B,O=eMudhra BV,C=NL",
|
||||
"issuer": "CN=emSign Root TLS CA - G3,O=eMudhra Technologies Limited,C=IN",
|
||||
"serial_hex": "6CADA367FB1C100088120EA202D568",
|
||||
"sha256": "98B896625C0C230ADEEC3EB2CF6AAD5001ECF9156A68644E82374CE5833BC25B",
|
||||
"spki_sha256": "43BFFA19D6EF651451F6841E019931E96F6F2EAEFF28AB4B6F1CE4576DE2717B",
|
||||
"subject_key_identifier": "9BD86551AFE7E72C37AD08EE6919D9BF3784AE0A",
|
||||
"authority_key_identifier": "E13AAB217CF1E34D54AED472B7FD8B5942209054",
|
||||
"key": {
|
||||
"algorithm": "EC",
|
||||
"curve": "secp384r1",
|
||||
"bits": 384
|
||||
},
|
||||
"signature_algorithm": "ecdsa-with-SHA384",
|
||||
"not_before": "2025-04-30T16:00:12Z",
|
||||
"not_after": "2040-04-28T18:30:00Z",
|
||||
"revocation": {
|
||||
"status": "revoked",
|
||||
"date": "2026-10-18T09:37:19Z",
|
||||
"reason": "ca_compromise",
|
||||
"crl_url": "http://crl.emsign.com/?roottlscag3.crl"
|
||||
},
|
||||
"pem": "-----BEGIN CERTIFICATE-----\nMIIDZjCCAu2gAwIBAgIPbK2jZ/scEACIEg6iAtVoMAoGCCqGSM49BAMDMFYxCzAJ\nBgNVBAYTAklOMSUwIwYDVQQKDBxlTXVkaHJhIFRlY2hub2xvZ2llcyBMaW1pdGVk\nMSAwHgYDVQQDDBdlbVNpZ24gUm9vdCBUTFMgQ0EgLSBHMzAeFw0yNTA0MzAxNjAw\nMTJaFw00MDA0MjgxODMwMDBaMD8xCzAJBgNVBAYTAk5MMRMwEQYDVQQKDAplTXVk\naHJhIEJWMRswGQYDVQQDDBJFTSBPViBUTFMgQ0EgLSBHM0IwdjAQBgcqhkjOPQIB\nBgUrgQQAIgNiAARziXqg6zz9yosSQC/68KvE4SErr43ud4rJ7i1tuD+sKAxeBWOK\n56Ks1xwyKo42HN659pxDArndjXPRzGyqsswK7k4hbOl53VGKSvS2JuVoTN1lgDft\nDIVOTMEvTPhkbQ2jggGWMIIBkjAfBgNVHSMEGDAWgBThOqshfPHjTVSu1HK3/YtZ\nQiCQVDAdBgNVHQ4EFgQUm9hlUa/n5yw3rQjuaRnZvzeErgowHQYDVR0lBBYwFAYI\nKwYBBQUHAwEGCCsGAQUFBwMCMF0GA1UdIARWMFQwOQYLKwYBBAGDjiEBAAEwKjAo\nBggrBgEFBQcCARYcaHR0cDovL3JlcG9zaXRvcnkuZW1zaWduLmNvbTANBgsrBgEE\nAYOOIQECbjAIBgZngQwBAgIwdQYIKwYBBQUHAQEEaTBnMCQGCCsGAQUFBzABhhho\ndHRwOi8vb2NzcC1iLmVtc2lnbi5jb20wPwYIKwYBBQUHMAKGM2h0dHA6Ly9yZXBv\nc2l0b3J5LmVtc2lnbi5jb20vY2VydHMvRU1PVlRMU0NBRzNCLnA3YzA3BgNVHR8E\nMDAuMCygKqAohiZodHRwOi8vY3JsLmVtc2lnbi5jb20vP3Jvb3R0bHNjYWczLmNy\nbDAOBgNVHQ8BAf8EBAMCAQYwEgYDVR0TAQH/BAgwBgEB/wIBADAKBggqhkjOPQQD\nAwNnADBkAjBAk+5BizdJhrED3wb7F2Gm7bWI60dlNd075nghT2TrR4NWKZtJZcbc\n28xxIY/NONICMF6mPdsjosaqmvEMWqKcBh4vsIvXSKnQj5o8zrFB4oKS+accjYTe\n8Dz3bQjauvy9uw==\n-----END CERTIFICATE-----"
|
||||
},
|
||||
{
|
||||
"name": "EM OV TLS CA - G3B",
|
||||
"usage": "tls",
|
||||
"issued_by": "AAA Certificate Services",
|
||||
"issuer_operator": "Sectigo",
|
||||
"issuer_sha256": "D7A7A0FB5D7E2731D771E9484EBCDEF71D5F0C3E0A2948782BC83EE0EA699EF4",
|
||||
"subject": "CN=EM OV TLS CA - G3B,O=eMudhra BV,C=NL",
|
||||
"issuer": "CN=AAA Certificate Services,O=Comodo CA Limited,L=Salford,ST=Greater Manchester,C=GB",
|
||||
"serial_hex": "E94223BDE92F914E5AB9ACABFDAC3710",
|
||||
"sha256": "5F1DED4DD590AF74631290CF073B122A2AAC6A1FA479778A0BD43579BD832699",
|
||||
"spki_sha256": "43BFFA19D6EF651451F6841E019931E96F6F2EAEFF28AB4B6F1CE4576DE2717B",
|
||||
"subject_key_identifier": "9BD86551AFE7E72C37AD08EE6919D9BF3784AE0A",
|
||||
"authority_key_identifier": "A0110A233E96F107ECE2AF29EF82A57FD030A4B4",
|
||||
"key": {
|
||||
"algorithm": "EC",
|
||||
"curve": "secp384r1",
|
||||
"bits": 384
|
||||
},
|
||||
"signature_algorithm": "sha256WithRSAEncryption",
|
||||
"not_before": "2025-05-14T00:00:00Z",
|
||||
"not_after": "2028-12-31T23:59:59Z",
|
||||
"revocation": {
|
||||
"status": "revoked",
|
||||
"date": "2026-10-19T10:35:36Z",
|
||||
"reason": "ca_compromise",
|
||||
"crl_url": "http://crl.comodoca.com/AAACertificateServices.crl"
|
||||
},
|
||||
"pem": "-----BEGIN CERTIFICATE-----\nMIIDzjCCAragAwIBAgIRAOlCI73pL5FOWrmsq/2sNxAwDQYJKoZIhvcNAQELBQAw\nezELMAkGA1UEBhMCR0IxGzAZBgNVBAgMEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4G\nA1UEBwwHU2FsZm9yZDEaMBgGA1UECgwRQ29tb2RvIENBIExpbWl0ZWQxITAfBgNV\nBAMMGEFBQSBDZXJ0aWZpY2F0ZSBTZXJ2aWNlczAeFw0yNTA1MTQwMDAwMDBaFw0y\nODEyMzEyMzU5NTlaMD8xCzAJBgNVBAYTAk5MMRMwEQYDVQQKDAplTXVkaHJhIEJW\nMRswGQYDVQQDDBJFTSBPViBUTFMgQ0EgLSBHM0IwdjAQBgcqhkjOPQIBBgUrgQQA\nIgNiAARziXqg6zz9yosSQC/68KvE4SErr43ud4rJ7i1tuD+sKAxeBWOK56Ks1xwy\nKo42HN659pxDArndjXPRzGyqsswK7k4hbOl53VGKSvS2JuVoTN1lgDftDIVOTMEv\nTPhkbQ2jggE1MIIBMTAfBgNVHSMEGDAWgBSgEQojPpbxB+zirynvgqV/0DCktDAd\nBgNVHQ4EFgQUm9hlUa/n5yw3rQjuaRnZvzeErgowDgYDVR0PAQH/BAQDAgEGMBIG\nA1UdEwEB/wQIMAYBAf8CAQAwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMC\nMDEGA1UdIAQqMCgwCAYGZ4EMAQICMA0GCysGAQQBg44hAQABMA0GCysGAQQBg44h\nAQJuMEMGA1UdHwQ8MDowOKA2oDSGMmh0dHA6Ly9jcmwuY29tb2RvY2EuY29tL0FB\nQUNlcnRpZmljYXRlU2VydmljZXMuY3JsMDQGCCsGAQUFBwEBBCgwJjAkBggrBgEF\nBQcwAYYYaHR0cDovL29jc3AuY29tb2RvY2EuY29tMA0GCSqGSIb3DQEBCwUAA4IB\nAQALRoOSvkzRa+tN5Rje9W5GiXd/qsicd9N6Gnh0SWC7VyWH+HycIqAnFCMYZznj\nCeGusomFYHE42SHWr85JDIbgzy6y3dO4i9n9w0Bx7yrXWBbDzFS8VgA3+EY4Cg7y\nmtoC3RPOJVtaXXPZ+68X804g1Mj+3vCSHj2lNPVgco0OzSIOJ/KMg1R9MLBkiKzb\nbERAGZHejY0HBhYd76IINZN8z6bBNah2UdFYxsOisLynPQUkol6gRLum024/FzVl\nCCDfnUusrkr+mAfPAvD/Vb8bhpA7jYtepuU+2mIdShj5lLlvAwZphn+ypWycuo2x\n0k9mldz2dqI9nhCiY+TTIOOR\n-----END CERTIFICATE-----"
|
||||
},
|
||||
{
|
||||
"name": "EM EV TLS CA - G3B",
|
||||
"usage": "tls",
|
||||
"issued_by": "emSign Root TLS CA - G3",
|
||||
"issuer_operator": "eMudhra",
|
||||
"issuer_sha256": "7DD78D5F4F13459A83DFF9ABBBA62EDBAF6F2D102BF257FD712F4D9F2746ED8D",
|
||||
"subject": "CN=EM EV TLS CA - G3B,O=eMudhra BV,C=NL",
|
||||
"issuer": "CN=emSign Root TLS CA - G3,O=eMudhra Technologies Limited,C=IN",
|
||||
"serial_hex": "C7C41EFE92D825F05EAE841A4A82C1",
|
||||
"sha256": "E7568FE6E12331F4FD7DE61DA4C7559D0F8EB192A218D4741A3BA75184333838",
|
||||
"spki_sha256": "E0254C7FCDC265E2F0F8A428DABB75AB4AE039B4872D62F5EE57F0877D5A8A41",
|
||||
"subject_key_identifier": "3EDC20D9894698877B873FD88147B25C15D84706",
|
||||
"authority_key_identifier": "E13AAB217CF1E34D54AED472B7FD8B5942209054",
|
||||
"key": {
|
||||
"algorithm": "EC",
|
||||
"curve": "secp384r1",
|
||||
"bits": 384
|
||||
},
|
||||
"signature_algorithm": "ecdsa-with-SHA384",
|
||||
"not_before": "2025-04-30T16:03:31Z",
|
||||
"not_after": "2040-04-28T18:30:00Z",
|
||||
"revocation": {
|
||||
"status": "revoked",
|
||||
"date": "2026-10-18T09:28:53Z",
|
||||
"reason": "ca_compromise",
|
||||
"crl_url": "http://crl.emsign.com/?roottlscag3.crl"
|
||||
},
|
||||
"pem": "-----BEGIN CERTIFICATE-----\nMIIDaDCCAu2gAwIBAgIQAMfEHv6S2CXwXq6EGkqCwTAKBggqhkjOPQQDAzBWMQsw\nCQYDVQQGEwJJTjElMCMGA1UECgwcZU11ZGhyYSBUZWNobm9sb2dpZXMgTGltaXRl\nZDEgMB4GA1UEAwwXZW1TaWduIFJvb3QgVExTIENBIC0gRzMwHhcNMjUwNDMwMTYw\nMzMxWhcNNDAwNDI4MTgzMDAwWjA/MQswCQYDVQQGEwJOTDETMBEGA1UECgwKZU11\nZGhyYSBCVjEbMBkGA1UEAwwSRU0gRVYgVExTIENBIC0gRzNCMHYwEAYHKoZIzj0C\nAQYFK4EEACIDYgAEJduoWmTTl9oLEFLp6lRuSHvv6pydnWigxerc/u91ppu11vwB\n9Cv7ym60C81kwHZ+Nv/QXZeoE8WqDGyVMOec9fCGZGjxOAZCCqXIUYQLHiUlQOeQ\n5LZUw4Uu1krx6GV9o4IBlTCCAZEwHwYDVR0jBBgwFoAU4TqrIXzx401UrtRyt/2L\nWUIgkFQwHQYDVR0OBBYEFD7cINmJRpiHe4c/2IFHslwV2EcGMB0GA1UdJQQWMBQG\nCCsGAQUFBwMBBggrBgEFBQcDAjBcBgNVHSAEVTBTMDkGCysGAQQBg44hAQABMCow\nKAYIKwYBBQUHAgEWHGh0dHA6Ly9yZXBvc2l0b3J5LmVtc2lnbi5jb20wDQYLKwYB\nBAGDjiEBAngwBwYFZ4EMAQEwdQYIKwYBBQUHAQEEaTBnMCQGCCsGAQUFBzABhhho\ndHRwOi8vb2NzcC1iLmVtc2lnbi5jb20wPwYIKwYBBQUHMAKGM2h0dHA6Ly9yZXBv\nc2l0b3J5LmVtc2lnbi5jb20vY2VydHMvRU1FVlRMU0NBRzNCLnA3YzA3BgNVHR8E\nMDAuMCygKqAohiZodHRwOi8vY3JsLmVtc2lnbi5jb20vP3Jvb3R0bHNjYWczLmNy\nbDAOBgNVHQ8BAf8EBAMCAQYwEgYDVR0TAQH/BAgwBgEB/wIBADAKBggqhkjOPQQD\nAwNpADBmAjEA9Jq999oaOUKhFwopw33iCmt0OHIn3gjT43GfuOHR/YckXdwoNdDA\nlIEhz0NgRO3/AjEAvY/a3OMvktIZLYjn+WLKmp0QhrIMV+e6EpS7j4GVeOKouNp6\nMN/P+ByGeiRMghzB\n-----END CERTIFICATE-----"
|
||||
},
|
||||
{
|
||||
"name": "EM EV TLS CA - G3B",
|
||||
"usage": "tls",
|
||||
"issued_by": "AAA Certificate Services",
|
||||
"issuer_operator": "Sectigo",
|
||||
"issuer_sha256": "D7A7A0FB5D7E2731D771E9484EBCDEF71D5F0C3E0A2948782BC83EE0EA699EF4",
|
||||
"subject": "CN=EM EV TLS CA - G3B,O=eMudhra BV,C=NL",
|
||||
"issuer": "CN=AAA Certificate Services,O=Comodo CA Limited,L=Salford,ST=Greater Manchester,C=GB",
|
||||
"serial_hex": "D8F8685E4F2BD9D82402A85A1C1EB731",
|
||||
"sha256": "579480AEA166290BD3C643589E795FC0D0E54E86505D180F1DFF99EFD3199512",
|
||||
"spki_sha256": "E0254C7FCDC265E2F0F8A428DABB75AB4AE039B4872D62F5EE57F0877D5A8A41",
|
||||
"subject_key_identifier": "3EDC20D9894698877B873FD88147B25C15D84706",
|
||||
"authority_key_identifier": "A0110A233E96F107ECE2AF29EF82A57FD030A4B4",
|
||||
"key": {
|
||||
"algorithm": "EC",
|
||||
"curve": "secp384r1",
|
||||
"bits": 384
|
||||
},
|
||||
"signature_algorithm": "sha256WithRSAEncryption",
|
||||
"not_before": "2025-05-14T00:00:00Z",
|
||||
"not_after": "2028-12-31T23:59:59Z",
|
||||
"revocation": {
|
||||
"status": "revoked",
|
||||
"date": "2026-10-19T10:35:36Z",
|
||||
"reason": "ca_compromise",
|
||||
"crl_url": "http://crl.comodoca.com/AAACertificateServices.crl"
|
||||
},
|
||||
"pem": "-----BEGIN CERTIFICATE-----\nMIIDzTCCArWgAwIBAgIRANj4aF5PK9nYJAKoWhwetzEwDQYJKoZIhvcNAQELBQAw\nezELMAkGA1UEBhMCR0IxGzAZBgNVBAgMEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4G\nA1UEBwwHU2FsZm9yZDEaMBgGA1UECgwRQ29tb2RvIENBIExpbWl0ZWQxITAfBgNV\nBAMMGEFBQSBDZXJ0aWZpY2F0ZSBTZXJ2aWNlczAeFw0yNTA1MTQwMDAwMDBaFw0y\nODEyMzEyMzU5NTlaMD8xCzAJBgNVBAYTAk5MMRMwEQYDVQQKDAplTXVkaHJhIEJW\nMRswGQYDVQQDDBJFTSBFViBUTFMgQ0EgLSBHM0IwdjAQBgcqhkjOPQIBBgUrgQQA\nIgNiAAQl26haZNOX2gsQUunqVG5Ie+/qnJ2daKDF6tz+73Wmm7XW/AH0K/vKbrQL\nzWTAdn42/9Bdl6gTxaoMbJUw55z18IZkaPE4BkIKpchRhAseJSVA55DktlTDhS7W\nSvHoZX2jggE0MIIBMDAfBgNVHSMEGDAWgBSgEQojPpbxB+zirynvgqV/0DCktDAd\nBgNVHQ4EFgQUPtwg2YlGmId7hz/YgUeyXBXYRwYwDgYDVR0PAQH/BAQDAgEGMBIG\nA1UdEwEB/wQIMAYBAf8CAQAwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMC\nMDAGA1UdIAQpMCcwBwYFZ4EMAQEwDQYLKwYBBAGDjiEBAAEwDQYLKwYBBAGDjiEB\nAngwQwYDVR0fBDwwOjA4oDagNIYyaHR0cDovL2NybC5jb21vZG9jYS5jb20vQUFB\nQ2VydGlmaWNhdGVTZXJ2aWNlcy5jcmwwNAYIKwYBBQUHAQEEKDAmMCQGCCsGAQUF\nBzABhhhodHRwOi8vb2NzcC5jb21vZG9jYS5jb20wDQYJKoZIhvcNAQELBQADggEB\nAJNFuAFW9Gnw/8+kCO4KIlZdnM1Sh2u+8cK0EYAHhXpb5FlGvYkcARigo5oS7VoM\n7FOYlTueV6yICZKNAYawZPJPhNCGK+rp3cQBEz6d0n8OZEYZov9lb3ll8ORgSDuf\nFVIW6Gagg/UnOamLBTOR8eD5V73SCme/B2cHUDw+m5E8cc+0Ujzz3Cn3ex5JrhXk\n2T3s8PoVzEmhTgGboFfcWO1EYhPPrYwdxPYRQIXAXUXhLEOVNL9ASbZJLFI8E1IM\nm7NwxVN2s38jWpaCUaaF0tg7hfiy7oe13rzJfHsdqldJVTTUTPWVx8TC4ju0VLet\nbAlwhwFT6AP8uIVMGUHNw24=\n-----END CERTIFICATE-----"
|
||||
},
|
||||
{
|
||||
"name": "EM DV TLS CA - G3C",
|
||||
"usage": "tls",
|
||||
"issued_by": "emSign Root TLS CA - G3",
|
||||
"issuer_operator": "eMudhra",
|
||||
"issuer_sha256": "7DD78D5F4F13459A83DFF9ABBBA62EDBAF6F2D102BF257FD712F4D9F2746ED8D",
|
||||
"subject": "CN=EM DV TLS CA - G3C,O=eMudhra Inc,C=US",
|
||||
"issuer": "CN=emSign Root TLS CA - G3,O=eMudhra Technologies Limited,C=IN",
|
||||
"serial_hex": "588B2E3C2BB8CDA13E1A1209254321",
|
||||
"sha256": "0164D7CEAE5BB909C272AF151FF44B1177AE9B47D9424B94F1D9558C30934D19",
|
||||
"spki_sha256": "49DD353DC2B919A3770645117D0E60C16115E7D1BC259EEFE734B00EA434FE5F",
|
||||
"subject_key_identifier": "AE7E105C3C2996C1A847270559F1E03DD4E50AF3",
|
||||
"authority_key_identifier": "E13AAB217CF1E34D54AED472B7FD8B5942209054",
|
||||
"key": {
|
||||
"algorithm": "EC",
|
||||
"curve": "secp384r1",
|
||||
"bits": 384
|
||||
},
|
||||
"signature_algorithm": "ecdsa-with-SHA384",
|
||||
"not_before": "2025-04-30T15:35:09Z",
|
||||
"not_after": "2040-04-28T18:30:00Z",
|
||||
"revocation": {
|
||||
"status": "revoked",
|
||||
"date": "2026-10-18T09:27:07Z",
|
||||
"reason": "ca_compromise",
|
||||
"crl_url": "http://crl.emsign.com/?roottlscag3.crl"
|
||||
},
|
||||
"pem": "-----BEGIN CERTIFICATE-----\nMIIDaDCCAu6gAwIBAgIPWIsuPCu4zaE+GhIJJUMhMAoGCCqGSM49BAMDMFYxCzAJ\nBgNVBAYTAklOMSUwIwYDVQQKDBxlTXVkaHJhIFRlY2hub2xvZ2llcyBMaW1pdGVk\nMSAwHgYDVQQDDBdlbVNpZ24gUm9vdCBUTFMgQ0EgLSBHMzAeFw0yNTA0MzAxNTM1\nMDlaFw00MDA0MjgxODMwMDBaMEAxCzAJBgNVBAYTAlVTMRQwEgYDVQQKDAtlTXVk\naHJhIEluYzEbMBkGA1UEAwwSRU0gRFYgVExTIENBIC0gRzNDMHYwEAYHKoZIzj0C\nAQYFK4EEACIDYgAEOnp4YsjETTJN3WkDOmeCwO/u/kBV5ooSP6A3G6O9kZRRTuFy\npDPjYw8CvRiAN9nlFJ+bd9uUooHfmHsv6oiLdL3+j9pMEJnubKGVZv4+8OX2sJpV\nbMyLpQVh3xsqRB0Bo4IBljCCAZIwHwYDVR0jBBgwFoAU4TqrIXzx401UrtRyt/2L\nWUIgkFQwHQYDVR0OBBYEFK5+EFw8KZbBqEcnBVnx4D3U5QrzMB0GA1UdJQQWMBQG\nCCsGAQUFBwMBBggrBgEFBQcDAjBdBgNVHSAEVjBUMDkGCysGAQQBg44hAQABMCow\nKAYIKwYBBQUHAgEWHGh0dHA6Ly9yZXBvc2l0b3J5LmVtc2lnbi5jb20wDQYLKwYB\nBAGDjiEBAmQwCAYGZ4EMAQIBMHUGCCsGAQUFBwEBBGkwZzAkBggrBgEFBQcwAYYY\naHR0cDovL29jc3AtYy5lbXNpZ24uY29tMD8GCCsGAQUFBzAChjNodHRwOi8vcmVw\nb3NpdG9yeS5lbXNpZ24uY29tL2NlcnRzL0VNRFZUTFNDQUczQy5wN2MwNwYDVR0f\nBDAwLjAsoCqgKIYmaHR0cDovL2NybC5lbXNpZ24uY29tLz9yb290dGxzY2FnMy5j\ncmwwDgYDVR0PAQH/BAQDAgEGMBIGA1UdEwEB/wQIMAYBAf8CAQAwCgYIKoZIzj0E\nAwMDaAAwZQIxAMEN2HWMocbuWdy1d33pH6JpMvP/V/DIS5PtZbv1Q7TXEbxrcyxg\nyuddOJscYDJJlAIwB4sH1BpbScDzkdD4tJBSwrG/1Mx82vb8YaySehNuu1+1oXNd\nAEKwaJ0mVzScy2+6\n-----END CERTIFICATE-----"
|
||||
},
|
||||
{
|
||||
"name": "EM DV TLS CA - G3C",
|
||||
"usage": "tls",
|
||||
"issued_by": "AAA Certificate Services",
|
||||
"issuer_operator": "Sectigo",
|
||||
"issuer_sha256": "D7A7A0FB5D7E2731D771E9484EBCDEF71D5F0C3E0A2948782BC83EE0EA699EF4",
|
||||
"subject": "CN=EM DV TLS CA - G3C,O=eMudhra Inc,C=US",
|
||||
"issuer": "CN=AAA Certificate Services,O=Comodo CA Limited,L=Salford,ST=Greater Manchester,C=GB",
|
||||
"serial_hex": "91F347F1A5B7D39962B95E0EE4E299D1",
|
||||
"sha256": "62B7F9868263F72F6911FC78CEAF72C1979158B7368BF648683BB2AA39B02E72",
|
||||
"spki_sha256": "49DD353DC2B919A3770645117D0E60C16115E7D1BC259EEFE734B00EA434FE5F",
|
||||
"subject_key_identifier": "AE7E105C3C2996C1A847270559F1E03DD4E50AF3",
|
||||
"authority_key_identifier": "A0110A233E96F107ECE2AF29EF82A57FD030A4B4",
|
||||
"key": {
|
||||
"algorithm": "EC",
|
||||
"curve": "secp384r1",
|
||||
"bits": 384
|
||||
},
|
||||
"signature_algorithm": "sha256WithRSAEncryption",
|
||||
"not_before": "2025-05-14T00:00:00Z",
|
||||
"not_after": "2028-12-31T23:59:59Z",
|
||||
"revocation": {
|
||||
"status": "revoked",
|
||||
"date": "2026-10-19T10:35:36Z",
|
||||
"reason": "ca_compromise",
|
||||
"crl_url": "http://crl.comodoca.com/AAACertificateServices.crl"
|
||||
},
|
||||
"pem": "-----BEGIN CERTIFICATE-----\nMIIDzzCCAregAwIBAgIRAJHzR/Glt9OZYrleDuTimdEwDQYJKoZIhvcNAQELBQAw\nezELMAkGA1UEBhMCR0IxGzAZBgNVBAgMEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4G\nA1UEBwwHU2FsZm9yZDEaMBgGA1UECgwRQ29tb2RvIENBIExpbWl0ZWQxITAfBgNV\nBAMMGEFBQSBDZXJ0aWZpY2F0ZSBTZXJ2aWNlczAeFw0yNTA1MTQwMDAwMDBaFw0y\nODEyMzEyMzU5NTlaMEAxCzAJBgNVBAYTAlVTMRQwEgYDVQQKDAtlTXVkaHJhIElu\nYzEbMBkGA1UEAwwSRU0gRFYgVExTIENBIC0gRzNDMHYwEAYHKoZIzj0CAQYFK4EE\nACIDYgAEOnp4YsjETTJN3WkDOmeCwO/u/kBV5ooSP6A3G6O9kZRRTuFypDPjYw8C\nvRiAN9nlFJ+bd9uUooHfmHsv6oiLdL3+j9pMEJnubKGVZv4+8OX2sJpVbMyLpQVh\n3xsqRB0Bo4IBNTCCATEwHwYDVR0jBBgwFoAUoBEKIz6W8Qfs4q8p74Klf9AwpLQw\nHQYDVR0OBBYEFK5+EFw8KZbBqEcnBVnx4D3U5QrzMA4GA1UdDwEB/wQEAwIBBjAS\nBgNVHRMBAf8ECDAGAQH/AgEAMB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcD\nAjAxBgNVHSAEKjAoMAgGBmeBDAECATANBgsrBgEEAYOOIQEAATANBgsrBgEEAYOO\nIQECZDBDBgNVHR8EPDA6MDigNqA0hjJodHRwOi8vY3JsLmNvbW9kb2NhLmNvbS9B\nQUFDZXJ0aWZpY2F0ZVNlcnZpY2VzLmNybDA0BggrBgEFBQcBAQQoMCYwJAYIKwYB\nBQUHMAGGGGh0dHA6Ly9vY3NwLmNvbW9kb2NhLmNvbTANBgkqhkiG9w0BAQsFAAOC\nAQEAr7b6Mx11TIJgVzHt6EH5qGNWeiiqoMrp8E5/k8s+jvupx1n083pZ5XOzUzND\nwEUe0W4C4FIGUHNQ4XDwqOw0yK42xqm2dvCKgsko2UvDlT0haZVtTCeWMQOe5J/J\nziaMfZwsZNfZ3aaI6XPtWDZtwY8AaNfyaeCpbtm5GcGnyNWk4jSNpbtpo6NjfXuP\nCFOm3rTjJ4zytP7Tso6iMm0L01cYFOlTmoPv4515R2r3SPnKDtijoB/D/vgxzojv\nDafgyG9afX8+laO9NbMMz4sqC1vZx3984i/sBNFiugm3Vvdq5OdJRjuGC1QILIs6\ndg/K3i8VQqRpbfaM9kIlMzZ3dQ==\n-----END CERTIFICATE-----"
|
||||
},
|
||||
{
|
||||
"name": "EM OV TLS CA - G3C",
|
||||
"usage": "tls",
|
||||
"issued_by": "emSign Root TLS CA - G3",
|
||||
"issuer_operator": "eMudhra",
|
||||
"issuer_sha256": "7DD78D5F4F13459A83DFF9ABBBA62EDBAF6F2D102BF257FD712F4D9F2746ED8D",
|
||||
"subject": "CN=EM OV TLS CA - G3C,O=eMudhra Inc,C=US",
|
||||
"issuer": "CN=emSign Root TLS CA - G3,O=eMudhra Technologies Limited,C=IN",
|
||||
"serial_hex": "296926C227A3A1526392B19482A504",
|
||||
"sha256": "CB6E15165AC802E9B3F993F16C25D41F4E81C21EBDE71AE1B012AC00BF9C7C3F",
|
||||
"spki_sha256": "63ECEFEE431D49523BC1136C4E72D0206A477086CAF606CD5CF025686C109CF8",
|
||||
"subject_key_identifier": "34EAEA0CEBC90ADAF9D0C5E2ED9B78C9879E1B6C",
|
||||
"authority_key_identifier": "E13AAB217CF1E34D54AED472B7FD8B5942209054",
|
||||
"key": {
|
||||
"algorithm": "EC",
|
||||
"curve": "secp384r1",
|
||||
"bits": 384
|
||||
},
|
||||
"signature_algorithm": "ecdsa-with-SHA384",
|
||||
"not_before": "2025-04-30T15:37:41Z",
|
||||
"not_after": "2040-04-28T18:30:00Z",
|
||||
"revocation": {
|
||||
"status": "revoked",
|
||||
"date": "2026-10-18T09:38:46Z",
|
||||
"reason": "ca_compromise",
|
||||
"crl_url": "http://crl.emsign.com/?roottlscag3.crl"
|
||||
},
|
||||
"pem": "-----BEGIN CERTIFICATE-----\nMIIDaDCCAu6gAwIBAgIPKWkmwiejoVJjkrGUgqUEMAoGCCqGSM49BAMDMFYxCzAJ\nBgNVBAYTAklOMSUwIwYDVQQKDBxlTXVkaHJhIFRlY2hub2xvZ2llcyBMaW1pdGVk\nMSAwHgYDVQQDDBdlbVNpZ24gUm9vdCBUTFMgQ0EgLSBHMzAeFw0yNTA0MzAxNTM3\nNDFaFw00MDA0MjgxODMwMDBaMEAxCzAJBgNVBAYTAlVTMRQwEgYDVQQKDAtlTXVk\naHJhIEluYzEbMBkGA1UEAwwSRU0gT1YgVExTIENBIC0gRzNDMHYwEAYHKoZIzj0C\nAQYFK4EEACIDYgAEE4JRzVKskpjByKrZdzId65fnCb0LTKCspV3IrVHc/J0ViaFZ\nfjpRIOHv1jHGPhg1ysrimGmmLhYx6KKBgatWYW3EXZGKvAnzqw5jz3kqpNztc4e+\nN7ulaVSfHAKycO1no4IBljCCAZIwHwYDVR0jBBgwFoAU4TqrIXzx401UrtRyt/2L\nWUIgkFQwHQYDVR0OBBYEFDTq6gzryQra+dDF4u2beMmHnhtsMB0GA1UdJQQWMBQG\nCCsGAQUFBwMBBggrBgEFBQcDAjBdBgNVHSAEVjBUMDkGCysGAQQBg44hAQABMCow\nKAYIKwYBBQUHAgEWHGh0dHA6Ly9yZXBvc2l0b3J5LmVtc2lnbi5jb20wDQYLKwYB\nBAGDjiEBAm4wCAYGZ4EMAQICMHUGCCsGAQUFBwEBBGkwZzAkBggrBgEFBQcwAYYY\naHR0cDovL29jc3AtYy5lbXNpZ24uY29tMD8GCCsGAQUFBzAChjNodHRwOi8vcmVw\nb3NpdG9yeS5lbXNpZ24uY29tL2NlcnRzL0VNT1ZUTFNDQUczQy5wN2MwNwYDVR0f\nBDAwLjAsoCqgKIYmaHR0cDovL2NybC5lbXNpZ24uY29tLz9yb290dGxzY2FnMy5j\ncmwwDgYDVR0PAQH/BAQDAgEGMBIGA1UdEwEB/wQIMAYBAf8CAQAwCgYIKoZIzj0E\nAwMDaAAwZQIxAMSZt2xsPeumRdeSMSQ3CbzIZeQ/q7GDCWfeE1Pq+mhgA6gRM44c\nNVF3xJ+wsxJk+wIwFJbaDMokD8Ei6eNOiA8yUMYvfzQyngmrhZQMdPCK1SewrR79\n4rSb9vGqxZzelQth\n-----END CERTIFICATE-----"
|
||||
},
|
||||
{
|
||||
"name": "EM OV TLS CA - G3C",
|
||||
"usage": "tls",
|
||||
"issued_by": "AAA Certificate Services",
|
||||
"issuer_operator": "Sectigo",
|
||||
"issuer_sha256": "D7A7A0FB5D7E2731D771E9484EBCDEF71D5F0C3E0A2948782BC83EE0EA699EF4",
|
||||
"subject": "CN=EM OV TLS CA - G3C,O=eMudhra Inc,C=US",
|
||||
"issuer": "CN=AAA Certificate Services,O=Comodo CA Limited,L=Salford,ST=Greater Manchester,C=GB",
|
||||
"serial_hex": "8D5A4811939EF447D77111CDD9849AE9",
|
||||
"sha256": "7485B51C0ED192878FA1D2F948FB529AE1842CC5086C4B67F7BE5B2039DE9489",
|
||||
"spki_sha256": "63ECEFEE431D49523BC1136C4E72D0206A477086CAF606CD5CF025686C109CF8",
|
||||
"subject_key_identifier": "34EAEA0CEBC90ADAF9D0C5E2ED9B78C9879E1B6C",
|
||||
"authority_key_identifier": "A0110A233E96F107ECE2AF29EF82A57FD030A4B4",
|
||||
"key": {
|
||||
"algorithm": "EC",
|
||||
"curve": "secp384r1",
|
||||
"bits": 384
|
||||
},
|
||||
"signature_algorithm": "sha256WithRSAEncryption",
|
||||
"not_before": "2025-05-14T00:00:00Z",
|
||||
"not_after": "2028-12-31T23:59:59Z",
|
||||
"revocation": {
|
||||
"status": "revoked",
|
||||
"date": "2026-10-19T10:35:36Z",
|
||||
"reason": "ca_compromise",
|
||||
"crl_url": "http://crl.comodoca.com/AAACertificateServices.crl"
|
||||
},
|
||||
"pem": "-----BEGIN CERTIFICATE-----\nMIIDzzCCAregAwIBAgIRAI1aSBGTnvRH13ERzdmEmukwDQYJKoZIhvcNAQELBQAw\nezELMAkGA1UEBhMCR0IxGzAZBgNVBAgMEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4G\nA1UEBwwHU2FsZm9yZDEaMBgGA1UECgwRQ29tb2RvIENBIExpbWl0ZWQxITAfBgNV\nBAMMGEFBQSBDZXJ0aWZpY2F0ZSBTZXJ2aWNlczAeFw0yNTA1MTQwMDAwMDBaFw0y\nODEyMzEyMzU5NTlaMEAxCzAJBgNVBAYTAlVTMRQwEgYDVQQKDAtlTXVkaHJhIElu\nYzEbMBkGA1UEAwwSRU0gT1YgVExTIENBIC0gRzNDMHYwEAYHKoZIzj0CAQYFK4EE\nACIDYgAEE4JRzVKskpjByKrZdzId65fnCb0LTKCspV3IrVHc/J0ViaFZfjpRIOHv\n1jHGPhg1ysrimGmmLhYx6KKBgatWYW3EXZGKvAnzqw5jz3kqpNztc4e+N7ulaVSf\nHAKycO1no4IBNTCCATEwHwYDVR0jBBgwFoAUoBEKIz6W8Qfs4q8p74Klf9AwpLQw\nHQYDVR0OBBYEFDTq6gzryQra+dDF4u2beMmHnhtsMA4GA1UdDwEB/wQEAwIBBjAS\nBgNVHRMBAf8ECDAGAQH/AgEAMB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcD\nAjAxBgNVHSAEKjAoMAgGBmeBDAECAjANBgsrBgEEAYOOIQEAATANBgsrBgEEAYOO\nIQECbjBDBgNVHR8EPDA6MDigNqA0hjJodHRwOi8vY3JsLmNvbW9kb2NhLmNvbS9B\nQUFDZXJ0aWZpY2F0ZVNlcnZpY2VzLmNybDA0BggrBgEFBQcBAQQoMCYwJAYIKwYB\nBQUHMAGGGGh0dHA6Ly9vY3NwLmNvbW9kb2NhLmNvbTANBgkqhkiG9w0BAQsFAAOC\nAQEAHlbhZpjhkPEAQVhPHRtBDaHqiisVD20pQ1TUeSx3l62Qe7NQ0b7vWQy7DN0i\nKOu7BGpP5EnIjvXbz9FhpfPJ4FhKEt3tQ51rqXz2oD8vRaryvExWPobDUz3j7HN6\nOzBsVMIe3PD7AAkpky0x8zMSi/ep5R9OMzybmNptbtppyuB5ytgq+h7lQdRIP+CQ\nTaJX3qTgfMqzntYre5GljmEB5hU2/rSn8zoSU2APkJidlZFpghtgbD4I2VRHZvCw\nFFr/SrFM9pwrgbJNZ1MtyTSVgI5NlNb0aMfLR+/r6jJt3ROeY2yh2QQ2dx+Yqgdb\nuL5ChFWOQU+ErSDnUq7yr+pjBQ==\n-----END CERTIFICATE-----"
|
||||
},
|
||||
{
|
||||
"name": "EM EV TLS CA - G3C",
|
||||
"usage": "tls",
|
||||
"issued_by": "emSign Root TLS CA - G3",
|
||||
"issuer_operator": "eMudhra",
|
||||
"issuer_sha256": "7DD78D5F4F13459A83DFF9ABBBA62EDBAF6F2D102BF257FD712F4D9F2746ED8D",
|
||||
"subject": "CN=EM EV TLS CA - G3C,O=eMudhra Inc,C=US",
|
||||
"issuer": "CN=emSign Root TLS CA - G3,O=eMudhra Technologies Limited,C=IN",
|
||||
"serial_hex": "8009928A64107601AF05FA2F19F109",
|
||||
"sha256": "432EC25F0D28DE9006D97BF752F17BBB2CD41F669933FCB072D9A1148D0261F3",
|
||||
"spki_sha256": "5C0660A2B164910D8FF589C18E14552E9E0EC237DCEFE2CB736D41A4811F81A0",
|
||||
"subject_key_identifier": "A6255E80A94DB7F3967C1BC791DF92BCEE4BEB5F",
|
||||
"authority_key_identifier": "E13AAB217CF1E34D54AED472B7FD8B5942209054",
|
||||
"key": {
|
||||
"algorithm": "EC",
|
||||
"curve": "secp384r1",
|
||||
"bits": 384
|
||||
},
|
||||
"signature_algorithm": "ecdsa-with-SHA384",
|
||||
"not_before": "2025-04-30T15:39:31Z",
|
||||
"not_after": "2040-04-28T18:30:00Z",
|
||||
"revocation": {
|
||||
"status": "revoked",
|
||||
"date": "2026-10-18T09:30:49Z",
|
||||
"reason": "ca_compromise",
|
||||
"crl_url": "http://crl.emsign.com/?roottlscag3.crl"
|
||||
},
|
||||
"pem": "-----BEGIN CERTIFICATE-----\nMIIDaDCCAu6gAwIBAgIQAIAJkopkEHYBrwX6LxnxCTAKBggqhkjOPQQDAzBWMQsw\nCQYDVQQGEwJJTjElMCMGA1UECgwcZU11ZGhyYSBUZWNobm9sb2dpZXMgTGltaXRl\nZDEgMB4GA1UEAwwXZW1TaWduIFJvb3QgVExTIENBIC0gRzMwHhcNMjUwNDMwMTUz\nOTMxWhcNNDAwNDI4MTgzMDAwWjBAMQswCQYDVQQGEwJVUzEUMBIGA1UECgwLZU11\nZGhyYSBJbmMxGzAZBgNVBAMMEkVNIEVWIFRMUyBDQSAtIEczQzB2MBAGByqGSM49\nAgEGBSuBBAAiA2IABC1W6Cmm+4LGSkaX2WCvlR6QZ66R+SbTQwbaSKiq63hptqZ+\nbSdCbgRCqgOzbhmI3/Y2UU7hzwf1uks/8qBmRa59MdWGatMao9JG6wsxd+6Zy3Vm\nxRbOdICKZo+R8MR/yqOCAZUwggGRMB8GA1UdIwQYMBaAFOE6qyF88eNNVK7Ucrf9\ni1lCIJBUMB0GA1UdDgQWBBSmJV6AqU2385Z8G8eR35K87kvrXzAdBgNVHSUEFjAU\nBggrBgEFBQcDAQYIKwYBBQUHAwIwXAYDVR0gBFUwUzA5BgsrBgEEAYOOIQEAATAq\nMCgGCCsGAQUFBwIBFhxodHRwOi8vcmVwb3NpdG9yeS5lbXNpZ24uY29tMA0GCysG\nAQQBg44hAQJ4MAcGBWeBDAEBMHUGCCsGAQUFBwEBBGkwZzAkBggrBgEFBQcwAYYY\naHR0cDovL29jc3AtYy5lbXNpZ24uY29tMD8GCCsGAQUFBzAChjNodHRwOi8vcmVw\nb3NpdG9yeS5lbXNpZ24uY29tL2NlcnRzL0VNRVZUTFNDQUczQy5wN2MwNwYDVR0f\nBDAwLjAsoCqgKIYmaHR0cDovL2NybC5lbXNpZ24uY29tLz9yb290dGxzY2FnMy5j\ncmwwDgYDVR0PAQH/BAQDAgEGMBIGA1UdEwEB/wQIMAYBAf8CAQAwCgYIKoZIzj0E\nAwMDaAAwZQIwBy1wTnzH4lXDTy2PD91ysLjvnlVN3JbJmc1BIMEU8TOLkYVHibMc\n9HeyonikFSfmAjEA6uE3QRxlrZTz/1/lXPTEMUhoauAtycagtlNFp79kjhvNa2/f\nAal1GNxsOutLnr0Y\n-----END CERTIFICATE-----"
|
||||
},
|
||||
{
|
||||
"name": "EM EV TLS CA - G3C",
|
||||
"usage": "tls",
|
||||
"issued_by": "AAA Certificate Services",
|
||||
"issuer_operator": "Sectigo",
|
||||
"issuer_sha256": "D7A7A0FB5D7E2731D771E9484EBCDEF71D5F0C3E0A2948782BC83EE0EA699EF4",
|
||||
"subject": "CN=EM EV TLS CA - G3C,O=eMudhra Inc,C=US",
|
||||
"issuer": "CN=AAA Certificate Services,O=Comodo CA Limited,L=Salford,ST=Greater Manchester,C=GB",
|
||||
"serial_hex": "05F078752DECE7A1BB8595D303167171",
|
||||
"sha256": "FD9D5C2DBBD4995AB69B8D50C439D9527B00529199C2FD01B2174EB58F07021F",
|
||||
"spki_sha256": "5C0660A2B164910D8FF589C18E14552E9E0EC237DCEFE2CB736D41A4811F81A0",
|
||||
"subject_key_identifier": "A6255E80A94DB7F3967C1BC791DF92BCEE4BEB5F",
|
||||
"authority_key_identifier": "A0110A233E96F107ECE2AF29EF82A57FD030A4B4",
|
||||
"key": {
|
||||
"algorithm": "EC",
|
||||
"curve": "secp384r1",
|
||||
"bits": 384
|
||||
},
|
||||
"signature_algorithm": "sha256WithRSAEncryption",
|
||||
"not_before": "2025-05-14T00:00:00Z",
|
||||
"not_after": "2028-12-31T23:59:59Z",
|
||||
"revocation": {
|
||||
"status": "revoked",
|
||||
"date": "2026-10-19T10:35:36Z",
|
||||
"reason": "ca_compromise",
|
||||
"crl_url": "http://crl.comodoca.com/AAACertificateServices.crl"
|
||||
},
|
||||
"pem": "-----BEGIN CERTIFICATE-----\nMIIDzTCCArWgAwIBAgIQBfB4dS3s56G7hZXTAxZxcTANBgkqhkiG9w0BAQsFADB7\nMQswCQYDVQQGEwJHQjEbMBkGA1UECAwSR3JlYXRlciBNYW5jaGVzdGVyMRAwDgYD\nVQQHDAdTYWxmb3JkMRowGAYDVQQKDBFDb21vZG8gQ0EgTGltaXRlZDEhMB8GA1UE\nAwwYQUFBIENlcnRpZmljYXRlIFNlcnZpY2VzMB4XDTI1MDUxNDAwMDAwMFoXDTI4\nMTIzMTIzNTk1OVowQDELMAkGA1UEBhMCVVMxFDASBgNVBAoMC2VNdWRocmEgSW5j\nMRswGQYDVQQDDBJFTSBFViBUTFMgQ0EgLSBHM0MwdjAQBgcqhkjOPQIBBgUrgQQA\nIgNiAAQtVugppvuCxkpGl9lgr5UekGeukfkm00MG2kioqut4abamfm0nQm4EQqoD\ns24ZiN/2NlFO4c8H9bpLP/KgZkWufTHVhmrTGqPSRusLMXfumct1ZsUWznSAimaP\nkfDEf8qjggE0MIIBMDAfBgNVHSMEGDAWgBSgEQojPpbxB+zirynvgqV/0DCktDAd\nBgNVHQ4EFgQUpiVegKlNt/OWfBvHkd+SvO5L618wDgYDVR0PAQH/BAQDAgEGMBIG\nA1UdEwEB/wQIMAYBAf8CAQAwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMC\nMDAGA1UdIAQpMCcwBwYFZ4EMAQEwDQYLKwYBBAGDjiEBAAEwDQYLKwYBBAGDjiEB\nAngwQwYDVR0fBDwwOjA4oDagNIYyaHR0cDovL2NybC5jb21vZG9jYS5jb20vQUFB\nQ2VydGlmaWNhdGVTZXJ2aWNlcy5jcmwwNAYIKwYBBQUHAQEEKDAmMCQGCCsGAQUF\nBzABhhhodHRwOi8vb2NzcC5jb21vZG9jYS5jb20wDQYJKoZIhvcNAQELBQADggEB\nAIFfHiaMSjnrdZygRYV4ekRBmG7ybcFk4hGQWMMyMFi0cAEk2+dGfz5bl8IaIbLq\nz+uVJzhOgVEX7WqIfgbWfFsfPYHYjPHBmy7SlZ3BwA86IQ6qF/etpX2/qL9A5yRN\nN3nVowUpaqV6H3aCpkAh3wR2ZNkvAbLDvPz3J0l+sEWjWv+ZISOV4R+hXDTdPXV0\n1lKxnbwqEsdo/sE2QkCDKwFrZh/0uKjWspydJj+jSaDLTrGNx50Dgmn7VEcD1ZI5\nFXjknVUiO25YlmS1dtVZSBbyUKSjlrP/yDwPcZunoAsmCAAtCKsFLviLaymzvsKI\neSSDGxpSySp2VCxompGDk8g=\n-----END CERTIFICATE-----"
|
||||
},
|
||||
{
|
||||
"name": "EM OV CS CA - G3B",
|
||||
"usage": "code-signing",
|
||||
"issued_by": "emSign Root CS CA - G3",
|
||||
"issuer_operator": "eMudhra",
|
||||
"issuer_sha256": "F2EDAAFF3D9B701FB4D14C9F285F8C0BAD90FC3BCC1FA07D47F4718EDABA34A5",
|
||||
"subject": "CN=EM OV CS CA - G3B,O=eMudhra BV,C=NL",
|
||||
"issuer": "CN=emSign Root CS CA - G3,O=eMudhra Technologies Limited,C=IN",
|
||||
"serial_hex": "7818933F8F85B4619A2B35FBA83D0A",
|
||||
"sha256": "9E39DE2881273ACC225A149C72BA3E0F51424963669AB633EE1DC7DF5DB067B4",
|
||||
"spki_sha256": "EC4FFA1AD0AC88081314B7AAFFEF6A91F97CD57291BB5BA686C5415F5EB4F6B6",
|
||||
"subject_key_identifier": "79FAE8ECCFDB0D758DA458D69FB6EDDC7C49DEE6",
|
||||
"authority_key_identifier": "687403F885CA8D5450571D16E0C8DE450DABCD2D",
|
||||
"key": {
|
||||
"algorithm": "EC",
|
||||
"curve": "secp384r1",
|
||||
"bits": 384
|
||||
},
|
||||
"signature_algorithm": "ecdsa-with-SHA384",
|
||||
"not_before": "2025-04-30T16:08:27Z",
|
||||
"not_after": "2040-04-28T18:30:00Z",
|
||||
"revocation": {
|
||||
"status": "revoked",
|
||||
"date": "2026-10-18T09:33:37Z",
|
||||
"reason": "ca_compromise",
|
||||
"crl_url": "http://crl.emsign.com/?rootcscag3.crl"
|
||||
},
|
||||
"pem": "-----BEGIN CERTIFICATE-----\nMIIDWzCCAuCgAwIBAgIPeBiTP4+FtGGaKzX7qD0KMAoGCCqGSM49BAMDMFUxCzAJ\nBgNVBAYTAklOMSUwIwYDVQQKDBxlTXVkaHJhIFRlY2hub2xvZ2llcyBMaW1pdGVk\nMR8wHQYDVQQDDBZlbVNpZ24gUm9vdCBDUyBDQSAtIEczMB4XDTI1MDQzMDE2MDgy\nN1oXDTQwMDQyODE4MzAwMFowPjELMAkGA1UEBhMCTkwxEzARBgNVBAoMCmVNdWRo\ncmEgQlYxGjAYBgNVBAMMEUVNIE9WIENTIENBIC0gRzNCMHYwEAYHKoZIzj0CAQYF\nK4EEACIDYgAEqrsPsGZqz88B+tUr/P1MK/LLPDSX3nOVDlgZth/mlQmHQcjEkUOb\nNKBDHK/+W/m0wcjorgAB+fTcitgsWfNjnpbNbD1NsM7+wmw9YlF7N3YTcnQ/jord\nd7EAZPKr5eBQo4IBizCCAYcwHwYDVR0jBBgwFoAUaHQD+IXKjVRQVx0W4MjeRQ2r\nzS0wHQYDVR0OBBYEFHn66OzP2w11jaRY1p+27dx8Sd7mMBMGA1UdJQQMMAoGCCsG\nAQUFBwMDMF4GA1UdIARXMFUwOQYLKwYBBAGDjiEBAAEwKjAoBggrBgEFBQcCARYc\naHR0cDovL3JlcG9zaXRvcnkuZW1zaWduLmNvbTAOBgwrBgEEAYOOIQECgUgwCAYG\nZ4EMAQQBMHQGCCsGAQUFBwEBBGgwZjAkBggrBgEFBQcwAYYYaHR0cDovL29jc3At\nYi5lbXNpZ24uY29tMD4GCCsGAQUFBzAChjJodHRwOi8vcmVwb3NpdG9yeS5lbXNp\nZ24uY29tL2NlcnRzL0VNT1ZDU0NBRzNCLnA3YzA2BgNVHR8ELzAtMCugKaAnhiVo\ndHRwOi8vY3JsLmVtc2lnbi5jb20vP3Jvb3Rjc2NhZzMuY3JsMA4GA1UdDwEB/wQE\nAwIBBjASBgNVHRMBAf8ECDAGAQH/AgEAMAoGCCqGSM49BAMDA2kAMGYCMQC/zr03\nYEELsNKqzUtH2dVFi4BaKFdyrr/6G1dIWp8KAVgsw2/u4lyyboUDaC9jDvICMQC9\nHTxku84tNX9dmHOtH7aC2Nu/JM0jTf6KjZAvovCJzzv1IFWSZpVLZibynMLXOrM=\n-----END CERTIFICATE-----"
|
||||
},
|
||||
{
|
||||
"name": "EM OV CS CA - G3B",
|
||||
"usage": "code-signing",
|
||||
"issued_by": "AAA Certificate Services",
|
||||
"issuer_operator": "Sectigo",
|
||||
"issuer_sha256": "D7A7A0FB5D7E2731D771E9484EBCDEF71D5F0C3E0A2948782BC83EE0EA699EF4",
|
||||
"subject": "CN=EM OV CS CA - G3B,O=eMudhra BV,C=NL",
|
||||
"issuer": "CN=AAA Certificate Services,O=Comodo CA Limited,L=Salford,ST=Greater Manchester,C=GB",
|
||||
"serial_hex": "41E32B73AC185D6CE0D7480BACF84E69",
|
||||
"sha256": "A496E23AC0CAB8155FC9B117B97EAE368EFCBB9041AEC8804D5A308D2D01C2D8",
|
||||
"spki_sha256": "EC4FFA1AD0AC88081314B7AAFFEF6A91F97CD57291BB5BA686C5415F5EB4F6B6",
|
||||
"subject_key_identifier": "79FAE8ECCFDB0D758DA458D69FB6EDDC7C49DEE6",
|
||||
"authority_key_identifier": "A0110A233E96F107ECE2AF29EF82A57FD030A4B4",
|
||||
"key": {
|
||||
"algorithm": "EC",
|
||||
"curve": "secp384r1",
|
||||
"bits": 384
|
||||
},
|
||||
"signature_algorithm": "sha256WithRSAEncryption",
|
||||
"not_before": "2025-05-14T00:00:00Z",
|
||||
"not_after": "2028-12-31T23:59:59Z",
|
||||
"revocation": {
|
||||
"status": "revoked",
|
||||
"date": "2026-10-19T10:35:36Z",
|
||||
"reason": "ca_compromise",
|
||||
"crl_url": "http://crl.comodoca.com/AAACertificateServices.crl"
|
||||
},
|
||||
"pem": "-----BEGIN CERTIFICATE-----\nMIIDwzCCAqugAwIBAgIQQeMrc6wYXWzg10gLrPhOaTANBgkqhkiG9w0BAQsFADB7\nMQswCQYDVQQGEwJHQjEbMBkGA1UECAwSR3JlYXRlciBNYW5jaGVzdGVyMRAwDgYD\nVQQHDAdTYWxmb3JkMRowGAYDVQQKDBFDb21vZG8gQ0EgTGltaXRlZDEhMB8GA1UE\nAwwYQUFBIENlcnRpZmljYXRlIFNlcnZpY2VzMB4XDTI1MDUxNDAwMDAwMFoXDTI4\nMTIzMTIzNTk1OVowPjELMAkGA1UEBhMCTkwxEzARBgNVBAoMCmVNdWRocmEgQlYx\nGjAYBgNVBAMMEUVNIE9WIENTIENBIC0gRzNCMHYwEAYHKoZIzj0CAQYFK4EEACID\nYgAEqrsPsGZqz88B+tUr/P1MK/LLPDSX3nOVDlgZth/mlQmHQcjEkUObNKBDHK/+\nW/m0wcjorgAB+fTcitgsWfNjnpbNbD1NsM7+wmw9YlF7N3YTcnQ/jordd7EAZPKr\n5eBQo4IBLDCCASgwHwYDVR0jBBgwFoAUoBEKIz6W8Qfs4q8p74Klf9AwpLQwHQYD\nVR0OBBYEFHn66OzP2w11jaRY1p+27dx8Sd7mMA4GA1UdDwEB/wQEAwIBBjASBgNV\nHRMBAf8ECDAGAQH/AgEAMBMGA1UdJQQMMAoGCCsGAQUFBwMDMDIGA1UdIAQrMCkw\nCAYGZ4EMAQQBMA0GCysGAQQBg44hAQABMA4GDCsGAQQBg44hAQKBSDBDBgNVHR8E\nPDA6MDigNqA0hjJodHRwOi8vY3JsLmNvbW9kb2NhLmNvbS9BQUFDZXJ0aWZpY2F0\nZVNlcnZpY2VzLmNybDA0BggrBgEFBQcBAQQoMCYwJAYIKwYBBQUHMAGGGGh0dHA6\nLy9vY3NwLmNvbW9kb2NhLmNvbTANBgkqhkiG9w0BAQsFAAOCAQEAIh0OzWsdBYhY\n7+yxPGyosRL+uUSN6/Mrd30fBuougs5FVWQ3cRgLhImnUUgXADY9buugWhMmfT3c\nyU5T1MFXZXClCILm5mfg+oqm/ikUWJl3Ayf2GvYf2O++6vzqvkn418nI5Lpdt0O7\nLi524YS/3CZHDA+zZQ7rrHpZXgQ9Hmp8AUQ4FneaGjNanFoYxiZxwCyW5z4u7AoK\n0bmOVqKtcDFoVNz5++G9TYDJdhWOXmKKgzqRo1MK0PjazNT7MGHqknP93SqMVJ1g\nmX/qNWxYteAjlvFanQxBNX/+TRxDjuFsEHO/7BvqSpIIck0UEEfizhN8zNpWLbcl\nEqVgl0g39A==\n-----END CERTIFICATE-----"
|
||||
},
|
||||
{
|
||||
"name": "EM OV CS CA - G3C",
|
||||
"usage": "code-signing",
|
||||
"issued_by": "emSign Root CS CA - G3",
|
||||
"issuer_operator": "eMudhra",
|
||||
"issuer_sha256": "F2EDAAFF3D9B701FB4D14C9F285F8C0BAD90FC3BCC1FA07D47F4718EDABA34A5",
|
||||
"subject": "CN=EM OV CS CA - G3C,O=eMudhra Inc,C=US",
|
||||
"issuer": "CN=emSign Root CS CA - G3,O=eMudhra Technologies Limited,C=IN",
|
||||
"serial_hex": "544B7F922C31AE220A4A922916F6E6",
|
||||
"sha256": "4B2460A466F95FD40113BCDCCA2CEB29B3B093921DE45474DA7DF48874C630B1",
|
||||
"spki_sha256": "92C063D67E8F14DB71D288E11266FFDAF69CBF115F7BAB14F3BC2F68D25E0E30",
|
||||
"subject_key_identifier": "EE6F73298D6FBFA7B31BC39534F61556447866B7",
|
||||
"authority_key_identifier": "687403F885CA8D5450571D16E0C8DE450DABCD2D",
|
||||
"key": {
|
||||
"algorithm": "EC",
|
||||
"curve": "secp384r1",
|
||||
"bits": 384
|
||||
},
|
||||
"signature_algorithm": "ecdsa-with-SHA384",
|
||||
"not_before": "2025-04-30T16:38:40Z",
|
||||
"not_after": "2040-04-28T18:30:00Z",
|
||||
"revocation": {
|
||||
"status": "revoked",
|
||||
"date": "2026-10-18T09:35:24Z",
|
||||
"reason": "ca_compromise",
|
||||
"crl_url": "http://crl.emsign.com/?rootcscag3.crl"
|
||||
},
|
||||
"pem": "-----BEGIN CERTIFICATE-----\nMIIDWzCCAuGgAwIBAgIPVEt/kiwxriIKSpIpFvbmMAoGCCqGSM49BAMDMFUxCzAJ\nBgNVBAYTAklOMSUwIwYDVQQKDBxlTXVkaHJhIFRlY2hub2xvZ2llcyBMaW1pdGVk\nMR8wHQYDVQQDDBZlbVNpZ24gUm9vdCBDUyBDQSAtIEczMB4XDTI1MDQzMDE2Mzg0\nMFoXDTQwMDQyODE4MzAwMFowPzELMAkGA1UEBhMCVVMxFDASBgNVBAoMC2VNdWRo\ncmEgSW5jMRowGAYDVQQDDBFFTSBPViBDUyBDQSAtIEczQzB2MBAGByqGSM49AgEG\nBSuBBAAiA2IABLnw4GNHAqacpqrdRDz9YsQbm6SyCSRpfXl052kaEm+5avs+NML9\nO8X0gwYfJl3CpYQ2NHMjUQQ2so8E84YjYwZ5gOJQPgJTLWVGI1pxZaJZoe9Jb3Qb\nX97neHII5Lj676OCAYswggGHMB8GA1UdIwQYMBaAFGh0A/iFyo1UUFcdFuDI3kUN\nq80tMB0GA1UdDgQWBBTub3MpjW+/p7Mbw5U09hVWRHhmtzATBgNVHSUEDDAKBggr\nBgEFBQcDAzBeBgNVHSAEVzBVMDkGCysGAQQBg44hAQABMCowKAYIKwYBBQUHAgEW\nHGh0dHA6Ly9yZXBvc2l0b3J5LmVtc2lnbi5jb20wDgYMKwYBBAGDjiEBAoFIMAgG\nBmeBDAEEATB0BggrBgEFBQcBAQRoMGYwJAYIKwYBBQUHMAGGGGh0dHA6Ly9vY3Nw\nLWMuZW1zaWduLmNvbTA+BggrBgEFBQcwAoYyaHR0cDovL3JlcG9zaXRvcnkuZW1z\naWduLmNvbS9jZXJ0cy9FTU9WQ1NDQUczQy5wN2MwNgYDVR0fBC8wLTAroCmgJ4Yl\naHR0cDovL2NybC5lbXNpZ24uY29tLz9yb290Y3NjYWczLmNybDAOBgNVHQ8BAf8E\nBAMCAQYwEgYDVR0TAQH/BAgwBgEB/wIBADAKBggqhkjOPQQDAwNoADBlAjEA8yPg\n8sntOhLwV2QXJ5LEaKSpo/fnE3uOgTadK73fDhyjiqr0Io9GVuQTYqBPbQ2TAjBg\neuApmBS486+iei03lRrp139dPw6k28jjGNvWsAU7A1N86J1zPJVDEyVH+nMAWC0=\n-----END CERTIFICATE-----"
|
||||
},
|
||||
{
|
||||
"name": "EM OV CS CA - G3C",
|
||||
"usage": "code-signing",
|
||||
"issued_by": "AAA Certificate Services",
|
||||
"issuer_operator": "Sectigo",
|
||||
"issuer_sha256": "D7A7A0FB5D7E2731D771E9484EBCDEF71D5F0C3E0A2948782BC83EE0EA699EF4",
|
||||
"subject": "CN=EM OV CS CA - G3C,O=eMudhra Inc,C=US",
|
||||
"issuer": "CN=AAA Certificate Services,O=Comodo CA Limited,L=Salford,ST=Greater Manchester,C=GB",
|
||||
"serial_hex": "D6A5EF613B390F32EF9CDF349F772841",
|
||||
"sha256": "84112972B8E59D14560AE7E7119654474AAD119990CF3722BFC1E3B91615E1FD",
|
||||
"spki_sha256": "92C063D67E8F14DB71D288E11266FFDAF69CBF115F7BAB14F3BC2F68D25E0E30",
|
||||
"subject_key_identifier": "EE6F73298D6FBFA7B31BC39534F61556447866B7",
|
||||
"authority_key_identifier": "A0110A233E96F107ECE2AF29EF82A57FD030A4B4",
|
||||
"key": {
|
||||
"algorithm": "EC",
|
||||
"curve": "secp384r1",
|
||||
"bits": 384
|
||||
},
|
||||
"signature_algorithm": "sha256WithRSAEncryption",
|
||||
"not_before": "2025-05-14T00:00:00Z",
|
||||
"not_after": "2028-12-31T23:59:59Z",
|
||||
"revocation": {
|
||||
"status": "revoked",
|
||||
"date": "2026-10-19T10:35:36Z",
|
||||
"reason": "ca_compromise",
|
||||
"crl_url": "http://crl.comodoca.com/AAACertificateServices.crl"
|
||||
},
|
||||
"pem": "-----BEGIN CERTIFICATE-----\nMIIDxTCCAq2gAwIBAgIRANal72E7OQ8y75zfNJ93KEEwDQYJKoZIhvcNAQELBQAw\nezELMAkGA1UEBhMCR0IxGzAZBgNVBAgMEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4G\nA1UEBwwHU2FsZm9yZDEaMBgGA1UECgwRQ29tb2RvIENBIExpbWl0ZWQxITAfBgNV\nBAMMGEFBQSBDZXJ0aWZpY2F0ZSBTZXJ2aWNlczAeFw0yNTA1MTQwMDAwMDBaFw0y\nODEyMzEyMzU5NTlaMD8xCzAJBgNVBAYTAlVTMRQwEgYDVQQKDAtlTXVkaHJhIElu\nYzEaMBgGA1UEAwwRRU0gT1YgQ1MgQ0EgLSBHM0MwdjAQBgcqhkjOPQIBBgUrgQQA\nIgNiAAS58OBjRwKmnKaq3UQ8/WLEG5uksgkkaX15dOdpGhJvuWr7PjTC/TvF9IMG\nHyZdwqWENjRzI1EENrKPBPOGI2MGeYDiUD4CUy1lRiNacWWiWaHvSW90G1/e53hy\nCOS4+u+jggEsMIIBKDAfBgNVHSMEGDAWgBSgEQojPpbxB+zirynvgqV/0DCktDAd\nBgNVHQ4EFgQU7m9zKY1vv6ezG8OVNPYVVkR4ZrcwDgYDVR0PAQH/BAQDAgEGMBIG\nA1UdEwEB/wQIMAYBAf8CAQAwEwYDVR0lBAwwCgYIKwYBBQUHAwMwMgYDVR0gBCsw\nKTAIBgZngQwBBAEwDQYLKwYBBAGDjiEBAAEwDgYMKwYBBAGDjiEBAoFIMEMGA1Ud\nHwQ8MDowOKA2oDSGMmh0dHA6Ly9jcmwuY29tb2RvY2EuY29tL0FBQUNlcnRpZmlj\nYXRlU2VydmljZXMuY3JsMDQGCCsGAQUFBwEBBCgwJjAkBggrBgEFBQcwAYYYaHR0\ncDovL29jc3AuY29tb2RvY2EuY29tMA0GCSqGSIb3DQEBCwUAA4IBAQBgGbZ+NDkn\n2Fr5dCVs/51+6cT5iEPP1stglX4TI2UaNh9LmPdltxXw3crV8gK5L8i+d+ScGibF\ny0obtBHNTldXDgLnAHIzY3n+pceyKj7O2ae9CYnyuu5RBifeI3XYKowWE+oXnVGF\nOy0CvCJ/JsdP6aYgoCa9hGnAYicYQQL6MOMQZ5PuhcSM8t4giIzrpgrEkUP93GYE\nemDG5Z8BYwKs80lLeQNBnwg/VHJ3iEF/o84RXYwrlrKqJ+33C094o6LNikMJCqOK\n/WYHZSR8Xyhf8GXFAj5pZuI6Bcpug2V/E7i42j8OBhc25aNYDqK+B0qmb90sjNUB\ng3tJeSIb/599\n-----END CERTIFICATE-----"
|
||||
}
|
||||
]
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIICPzCCAcagAwIBAgIUH0gWfBRnOiSjcduti/cukjDGAjIwCgYIKoZIzj0EAwIw
|
||||
PzELMAkGA1UEBhMCTkwxEzARBgNVBAoMCmVNdWRocmEgQlYxGzAZBgNVBAMMEkVN
|
||||
IE9WIFRMUyBDQSAtIEczQjAeFw0yNjEwMjAwMjIyNDJaFw0yNzA1MDMwMjI3NDJa
|
||||
MDMxMTAvBgNVBAMMKG92LWczYi1hYWEtZW11ZGhyYS53ZS1oYXZlLWEtcHJvYmxl
|
||||
bS5jb20wWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAAQ3EfjVE+6fUm9gr7UoUv6N
|
||||
YPttIdILR9OMZnAdTj+RWT1Z4fp7dpC5mat1gbIo4J6EDelcOQQz5/oGeB3gFJKw
|
||||
o4GrMIGoMAwGA1UdEwEB/wQCMAAwDgYDVR0PAQH/BAQDAgeAMBMGA1UdJQQMMAoG
|
||||
CCsGAQUFBwMBMDMGA1UdEQQsMCqCKG92LWczYi1hYWEtZW11ZGhyYS53ZS1oYXZl
|
||||
LWEtcHJvYmxlbS5jb20wHQYDVR0OBBYEFKzQPp6gTBOWb0TBxZbkL5zsQn3oMB8G
|
||||
A1UdIwQYMBaAFJvYZVGv5+csN60I7mkZ2b83hK4KMAoGCCqGSM49BAMCA2cAMGQC
|
||||
MARKrRRBEWY2Ix1d80DGUvC7bWENP1Q9pf6BOGF4+84UiwV5+nAuht/+hdpAanWB
|
||||
sQIwJF2dW8tpPN6lTm1kDerzG6yJAJOz4drotaTthMF2+efuv5W2FL7Dr8qZSd1z
|
||||
201o
|
||||
-----END CERTIFICATE-----
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIDzjCCAragAwIBAgIRAOlCI73pL5FOWrmsq/2sNxAwDQYJKoZIhvcNAQELBQAw
|
||||
ezELMAkGA1UEBhMCR0IxGzAZBgNVBAgMEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4G
|
||||
A1UEBwwHU2FsZm9yZDEaMBgGA1UECgwRQ29tb2RvIENBIExpbWl0ZWQxITAfBgNV
|
||||
BAMMGEFBQSBDZXJ0aWZpY2F0ZSBTZXJ2aWNlczAeFw0yNTA1MTQwMDAwMDBaFw0y
|
||||
ODEyMzEyMzU5NTlaMD8xCzAJBgNVBAYTAk5MMRMwEQYDVQQKDAplTXVkaHJhIEJW
|
||||
MRswGQYDVQQDDBJFTSBPViBUTFMgQ0EgLSBHM0IwdjAQBgcqhkjOPQIBBgUrgQQA
|
||||
IgNiAARziXqg6zz9yosSQC/68KvE4SErr43ud4rJ7i1tuD+sKAxeBWOK56Ks1xwy
|
||||
Ko42HN659pxDArndjXPRzGyqsswK7k4hbOl53VGKSvS2JuVoTN1lgDftDIVOTMEv
|
||||
TPhkbQ2jggE1MIIBMTAfBgNVHSMEGDAWgBSgEQojPpbxB+zirynvgqV/0DCktDAd
|
||||
BgNVHQ4EFgQUm9hlUa/n5yw3rQjuaRnZvzeErgowDgYDVR0PAQH/BAQDAgEGMBIG
|
||||
A1UdEwEB/wQIMAYBAf8CAQAwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMC
|
||||
MDEGA1UdIAQqMCgwCAYGZ4EMAQICMA0GCysGAQQBg44hAQABMA0GCysGAQQBg44h
|
||||
AQJuMEMGA1UdHwQ8MDowOKA2oDSGMmh0dHA6Ly9jcmwuY29tb2RvY2EuY29tL0FB
|
||||
QUNlcnRpZmljYXRlU2VydmljZXMuY3JsMDQGCCsGAQUFBwEBBCgwJjAkBggrBgEF
|
||||
BQcwAYYYaHR0cDovL29jc3AuY29tb2RvY2EuY29tMA0GCSqGSIb3DQEBCwUAA4IB
|
||||
AQALRoOSvkzRa+tN5Rje9W5GiXd/qsicd9N6Gnh0SWC7VyWH+HycIqAnFCMYZznj
|
||||
CeGusomFYHE42SHWr85JDIbgzy6y3dO4i9n9w0Bx7yrXWBbDzFS8VgA3+EY4Cg7y
|
||||
mtoC3RPOJVtaXXPZ+68X804g1Mj+3vCSHj2lNPVgco0OzSIOJ/KMg1R9MLBkiKzb
|
||||
bERAGZHejY0HBhYd76IINZN8z6bBNah2UdFYxsOisLynPQUkol6gRLum024/FzVl
|
||||
CCDfnUusrkr+mAfPAvD/Vb8bhpA7jYtepuU+2mIdShj5lLlvAwZphn+ypWycuo2x
|
||||
0k9mldz2dqI9nhCiY+TTIOOR
|
||||
-----END CERTIFICATE-----
|
||||
@@ -0,0 +1,56 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIICODCCAb6gAwIBAgIUPXjC97k7buQHZ7p5ehDU8QNyrR4wCgYIKoZIzj0EAwIw
|
||||
PzELMAkGA1UEBhMCTkwxEzARBgNVBAoMCmVNdWRocmEgQlYxGzAZBgNVBAMMEkVN
|
||||
IE9WIFRMUyBDQSAtIEczQjAeFw0yNjEwMjAwMDU5MzdaFw0yNzA1MDMwMTA0Mzda
|
||||
MC8xLTArBgNVBAMMJG92LWczYi1lbXVkaHJhLndlLWhhdmUtYS1wcm9ibGVtLmNv
|
||||
bTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABA1haD+95Albk0UN6YtDCPpqevTv
|
||||
YUmEhsIVpb/hnhsbBlH0yLthSE2JLH14ywiQJXttoc/pDWC9eoICW6CJXL2jgacw
|
||||
gaQwDAYDVR0TAQH/BAIwADAOBgNVHQ8BAf8EBAMCB4AwEwYDVR0lBAwwCgYIKwYB
|
||||
BQUHAwEwLwYDVR0RBCgwJoIkb3YtZzNiLWVtdWRocmEud2UtaGF2ZS1hLXByb2Js
|
||||
ZW0uY29tMB0GA1UdDgQWBBQUhzc4s202Hn62G0srmgguYdY2MTAfBgNVHSMEGDAW
|
||||
gBSb2GVRr+fnLDetCO5pGdm/N4SuCjAKBggqhkjOPQQDAgNoADBlAjEAoJ6d4zzA
|
||||
xE349yGHHrwKZpVCC/HBFutcOpOfzQoe8ikbmaaBTdtKai5TEllwt3dBAjBeHGiX
|
||||
VheDhJfewbei99cw1MfYB5tPwdfuY5pe1MJz0Tg9zJ4JgxBvF6LTT0OOXZ4=
|
||||
-----END CERTIFICATE-----
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIDZjCCAu2gAwIBAgIPbK2jZ/scEACIEg6iAtVoMAoGCCqGSM49BAMDMFYxCzAJ
|
||||
BgNVBAYTAklOMSUwIwYDVQQKDBxlTXVkaHJhIFRlY2hub2xvZ2llcyBMaW1pdGVk
|
||||
MSAwHgYDVQQDDBdlbVNpZ24gUm9vdCBUTFMgQ0EgLSBHMzAeFw0yNTA0MzAxNjAw
|
||||
MTJaFw00MDA0MjgxODMwMDBaMD8xCzAJBgNVBAYTAk5MMRMwEQYDVQQKDAplTXVk
|
||||
aHJhIEJWMRswGQYDVQQDDBJFTSBPViBUTFMgQ0EgLSBHM0IwdjAQBgcqhkjOPQIB
|
||||
BgUrgQQAIgNiAARziXqg6zz9yosSQC/68KvE4SErr43ud4rJ7i1tuD+sKAxeBWOK
|
||||
56Ks1xwyKo42HN659pxDArndjXPRzGyqsswK7k4hbOl53VGKSvS2JuVoTN1lgDft
|
||||
DIVOTMEvTPhkbQ2jggGWMIIBkjAfBgNVHSMEGDAWgBThOqshfPHjTVSu1HK3/YtZ
|
||||
QiCQVDAdBgNVHQ4EFgQUm9hlUa/n5yw3rQjuaRnZvzeErgowHQYDVR0lBBYwFAYI
|
||||
KwYBBQUHAwEGCCsGAQUFBwMCMF0GA1UdIARWMFQwOQYLKwYBBAGDjiEBAAEwKjAo
|
||||
BggrBgEFBQcCARYcaHR0cDovL3JlcG9zaXRvcnkuZW1zaWduLmNvbTANBgsrBgEE
|
||||
AYOOIQECbjAIBgZngQwBAgIwdQYIKwYBBQUHAQEEaTBnMCQGCCsGAQUFBzABhhho
|
||||
dHRwOi8vb2NzcC1iLmVtc2lnbi5jb20wPwYIKwYBBQUHMAKGM2h0dHA6Ly9yZXBv
|
||||
c2l0b3J5LmVtc2lnbi5jb20vY2VydHMvRU1PVlRMU0NBRzNCLnA3YzA3BgNVHR8E
|
||||
MDAuMCygKqAohiZodHRwOi8vY3JsLmVtc2lnbi5jb20vP3Jvb3R0bHNjYWczLmNy
|
||||
bDAOBgNVHQ8BAf8EBAMCAQYwEgYDVR0TAQH/BAgwBgEB/wIBADAKBggqhkjOPQQD
|
||||
AwNnADBkAjBAk+5BizdJhrED3wb7F2Gm7bWI60dlNd075nghT2TrR4NWKZtJZcbc
|
||||
28xxIY/NONICMF6mPdsjosaqmvEMWqKcBh4vsIvXSKnQj5o8zrFB4oKS+accjYTe
|
||||
8Dz3bQjauvy9uw==
|
||||
-----END CERTIFICATE-----
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIDdDCCAvmgAwIBAgIQAJYaPvsTWVdu0EAFtGhKWzAKBggqhkjOPQQDAzBrMQsw
|
||||
CQYDVQQGEwJJTjETMBEGA1UECxMKZW1TaWduIFBLSTElMCMGA1UEChMcZU11ZGhy
|
||||
YSBUZWNobm9sb2dpZXMgTGltaXRlZDEgMB4GA1UEAxMXZW1TaWduIEVDQyBSb290
|
||||
IENBIC0gRzMwHhcNMjQwNzExMTU0MzM2WhcNMzkwNzA5MTgzMDAwWjBWMQswCQYD
|
||||
VQQGEwJJTjElMCMGA1UECgwcZU11ZGhyYSBUZWNobm9sb2dpZXMgTGltaXRlZDEg
|
||||
MB4GA1UEAwwXZW1TaWduIFJvb3QgVExTIENBIC0gRzMwdjAQBgcqhkjOPQIBBgUr
|
||||
gQQAIgNiAARfD9w/q1cWxL6D9bMGH6CQ4u+gAV6GN9s+PwQx9Bl4hC+0iU8QOk8m
|
||||
WO/RwXQy10DCSxT8vSIJClHKp+Ao4qJKdYeV0SwZ7LMQziyLfxIUOY6Grja/sQFw
|
||||
LKYf8EdDXAyjggF1MIIBcTAfBgNVHSMEGDAWgBR8XQKEE9TMipuBzhccLikenEhj
|
||||
QjAdBgNVHQ4EFgQU4TqrIXzx401UrtRyt/2LWUIgkFQwHQYDVR0lBBYwFAYIKwYB
|
||||
BQUHAwEGCCsGAQUFBwMCMD0GA1UdIAQ2MDQwMgYEVR0gADAqMCgGCCsGAQUFBwIB
|
||||
FhxodHRwOi8vcmVwb3NpdG9yeS5lbXNpZ24uY29tMHgGCCsGAQUFBwEBBGwwajAi
|
||||
BggrBgEFBQcwAYYWaHR0cDovL29jc3AuZW1zaWduLmNvbTBEBggrBgEFBQcwAoY4
|
||||
aHR0cDovL3JlcG9zaXRvcnkuZW1zaWduLmNvbS9jZXJ0cy9lbVNpZ25FQ0NSb290
|
||||
Q0FHMy5jcnQwMwYDVR0fBCwwKjAooCagJIYiaHR0cDovL2NybC5lbXNpZ24uY29t
|
||||
P1Jvb3RDQUczLmNybDAOBgNVHQ8BAf8EBAMCAQYwEgYDVR0TAQH/BAgwBgEB/wIB
|
||||
ATAKBggqhkjOPQQDAwNpADBmAjEAt9CIpRGp46QHQYDUBnLKw+vvK0/h9vmilEAg
|
||||
TroUl/anH4x2rXHqiCtBTBfgKDegAjEA8WpG1LTZdAIL0FSYqxXeREaeTSxEofM0
|
||||
1jJH3jGvCXC79LU9UPJ2o3TU+UIj2gMV
|
||||
-----END CERTIFICATE-----
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIICQTCCAcegAwIBAgIULuWnWW6kFV6QZxp2ls8r18NCjXkwCgYIKoZIzj0EAwIw
|
||||
QDELMAkGA1UEBhMCVVMxFDASBgNVBAoMC2VNdWRocmEgSW5jMRswGQYDVQQDDBJF
|
||||
TSBPViBUTFMgQ0EgLSBHM0MwHhcNMjYxMDIwMDIyMjQyWhcNMjcwNTAzMDIyNzQy
|
||||
WjAzMTEwLwYDVQQDDChvdi1nM2MtYWFhLWVtdWRocmEud2UtaGF2ZS1hLXByb2Js
|
||||
ZW0uY29tMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEb7+5g2YHNYwH2eQERrZc
|
||||
TmVuf/RmvzsGGMSwXmZke6vn35Ut5HME7UOOU3Muy+BSfjfpo6fjL5KOs3Ndz67d
|
||||
OKOBqzCBqDAMBgNVHRMBAf8EAjAAMA4GA1UdDwEB/wQEAwIHgDATBgNVHSUEDDAK
|
||||
BggrBgEFBQcDATAzBgNVHREELDAqgihvdi1nM2MtYWFhLWVtdWRocmEud2UtaGF2
|
||||
ZS1hLXByb2JsZW0uY29tMB0GA1UdDgQWBBQ7oBzq3jpTuvntbIkzMV12Ps3O0TAf
|
||||
BgNVHSMEGDAWgBQ06uoM68kK2vnQxeLtm3jJh54bbDAKBggqhkjOPQQDAgNoADBl
|
||||
AjEA/Y7AZ4M2MYbN5Od9tREaUfFjZKoOjczfIKPjKNMjtGxTdL9WUXvAT4QDgktn
|
||||
/IJdAjAbORTkAXD5hggyG6K7p0MBz3lFZJb2l/LndZlrttJWBLP8QGrc44hrZZtK
|
||||
Iq0d1dE=
|
||||
-----END CERTIFICATE-----
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIDzzCCAregAwIBAgIRAI1aSBGTnvRH13ERzdmEmukwDQYJKoZIhvcNAQELBQAw
|
||||
ezELMAkGA1UEBhMCR0IxGzAZBgNVBAgMEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4G
|
||||
A1UEBwwHU2FsZm9yZDEaMBgGA1UECgwRQ29tb2RvIENBIExpbWl0ZWQxITAfBgNV
|
||||
BAMMGEFBQSBDZXJ0aWZpY2F0ZSBTZXJ2aWNlczAeFw0yNTA1MTQwMDAwMDBaFw0y
|
||||
ODEyMzEyMzU5NTlaMEAxCzAJBgNVBAYTAlVTMRQwEgYDVQQKDAtlTXVkaHJhIElu
|
||||
YzEbMBkGA1UEAwwSRU0gT1YgVExTIENBIC0gRzNDMHYwEAYHKoZIzj0CAQYFK4EE
|
||||
ACIDYgAEE4JRzVKskpjByKrZdzId65fnCb0LTKCspV3IrVHc/J0ViaFZfjpRIOHv
|
||||
1jHGPhg1ysrimGmmLhYx6KKBgatWYW3EXZGKvAnzqw5jz3kqpNztc4e+N7ulaVSf
|
||||
HAKycO1no4IBNTCCATEwHwYDVR0jBBgwFoAUoBEKIz6W8Qfs4q8p74Klf9AwpLQw
|
||||
HQYDVR0OBBYEFDTq6gzryQra+dDF4u2beMmHnhtsMA4GA1UdDwEB/wQEAwIBBjAS
|
||||
BgNVHRMBAf8ECDAGAQH/AgEAMB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcD
|
||||
AjAxBgNVHSAEKjAoMAgGBmeBDAECAjANBgsrBgEEAYOOIQEAATANBgsrBgEEAYOO
|
||||
IQECbjBDBgNVHR8EPDA6MDigNqA0hjJodHRwOi8vY3JsLmNvbW9kb2NhLmNvbS9B
|
||||
QUFDZXJ0aWZpY2F0ZVNlcnZpY2VzLmNybDA0BggrBgEFBQcBAQQoMCYwJAYIKwYB
|
||||
BQUHMAGGGGh0dHA6Ly9vY3NwLmNvbW9kb2NhLmNvbTANBgkqhkiG9w0BAQsFAAOC
|
||||
AQEAHlbhZpjhkPEAQVhPHRtBDaHqiisVD20pQ1TUeSx3l62Qe7NQ0b7vWQy7DN0i
|
||||
KOu7BGpP5EnIjvXbz9FhpfPJ4FhKEt3tQ51rqXz2oD8vRaryvExWPobDUz3j7HN6
|
||||
OzBsVMIe3PD7AAkpky0x8zMSi/ep5R9OMzybmNptbtppyuB5ytgq+h7lQdRIP+CQ
|
||||
TaJX3qTgfMqzntYre5GljmEB5hU2/rSn8zoSU2APkJidlZFpghtgbD4I2VRHZvCw
|
||||
FFr/SrFM9pwrgbJNZ1MtyTSVgI5NlNb0aMfLR+/r6jJt3ROeY2yh2QQ2dx+Yqgdb
|
||||
uL5ChFWOQU+ErSDnUq7yr+pjBQ==
|
||||
-----END CERTIFICATE-----
|
||||
Loaded 100 of 104 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user