From 59bc0a08e0facbd98b98d4d21a4f91b7f0df780c Mon Sep 17 00:00:00 2001 From: Jet Date: Tue, 5 May 2026 17:28:02 -0700 Subject: [PATCH 1/6] Add browser-based CLI login support Expose the CLI login API in generated SDKs and add Node CLI login/logout commands so saved credentials can be managed end to end. --- openapi/primitive-api.codegen.json | 390 +- openapi/primitive-api.yaml | 266 ++ sdk-go/api/oas_cfg_gen.go | 1 + sdk-go/api/oas_client_gen.go | 292 ++ sdk-go/api/oas_handlers_gen.go | 478 +++ sdk-go/api/oas_interfaces_gen.go | 12 + sdk-go/api/oas_json_gen.go | 3263 ++++++++++++----- sdk-go/api/oas_operations_gen.go | 3 + sdk-go/api/oas_request_decoders_gen.go | 244 ++ sdk-go/api/oas_request_encoders_gen.go | 54 + sdk-go/api/oas_response_decoders_gen.go | 706 +++- sdk-go/api/oas_response_encoders_gen.go | 255 ++ sdk-go/api/oas_router_gen.go | 311 +- sdk-go/api/oas_schemas_gen.go | 597 +++ sdk-go/api/oas_security_gen.go | 1 + sdk-go/api/oas_server_gen.go | 23 + sdk-go/api/oas_unimplemented_gen.go | 32 + sdk-go/api/oas_validators_gen.go | 239 ++ sdk-node/package.json | 3 + sdk-node/src/api/generated/index.ts | 4 +- sdk-node/src/api/generated/sdk.gen.ts | 53 +- sdk-node/src/api/generated/types.gen.ts | 174 +- sdk-node/src/oclif/api-command.ts | 59 +- sdk-node/src/oclif/auth.ts | 162 + sdk-node/src/oclif/commands/emails-latest.ts | 22 +- sdk-node/src/oclif/commands/login.ts | 267 ++ sdk-node/src/oclif/commands/logout.ts | 99 + sdk-node/src/oclif/commands/send.ts | 38 +- sdk-node/src/oclif/commands/whoami.ts | 22 +- sdk-node/src/oclif/fish-completion.ts | 2 +- sdk-node/src/oclif/index.ts | 6 + sdk-node/src/openapi/openapi.generated.ts | 392 +- sdk-node/src/openapi/operations.generated.ts | 177 + sdk-node/tests/oclif/api-command.test.ts | 100 + sdk-node/tests/oclif/auth.test.ts | 95 + sdk-node/tests/oclif/describe-command.test.ts | 5 + sdk-node/tests/oclif/login.test.ts | 110 + .../src/primitive/api/api/cli/__init__.py | 1 + .../src/primitive/api/api/cli/cli_logout.py | 223 ++ .../primitive/api/api/cli/poll_cli_login.py | 217 ++ .../primitive/api/api/cli/start_cli_login.py | 213 ++ .../src/primitive/api/models/__init__.py | 20 + .../api/models/cli_login_poll_result.py | 123 + .../api/models/cli_login_start_result.py | 116 + .../primitive/api/models/cli_logout_input.py | 70 + .../api/models/cli_logout_response_200.py | 100 + .../primitive/api/models/cli_logout_result.py | 88 + .../api/models/error_response_error_code.py | 5 + .../api/models/poll_cli_login_input.py | 58 + .../api/models/poll_cli_login_response_200.py | 100 + .../api/models/start_cli_login_input.py | 83 + .../models/start_cli_login_input_metadata.py | 66 + .../models/start_cli_login_response_201.py | 100 + 53 files changed, 9560 insertions(+), 980 deletions(-) create mode 100644 sdk-node/src/oclif/auth.ts create mode 100644 sdk-node/src/oclif/commands/login.ts create mode 100644 sdk-node/src/oclif/commands/logout.ts create mode 100644 sdk-node/tests/oclif/auth.test.ts create mode 100644 sdk-node/tests/oclif/login.test.ts create mode 100644 sdk-python/src/primitive/api/api/cli/__init__.py create mode 100644 sdk-python/src/primitive/api/api/cli/cli_logout.py create mode 100644 sdk-python/src/primitive/api/api/cli/poll_cli_login.py create mode 100644 sdk-python/src/primitive/api/api/cli/start_cli_login.py create mode 100644 sdk-python/src/primitive/api/models/cli_login_poll_result.py create mode 100644 sdk-python/src/primitive/api/models/cli_login_start_result.py create mode 100644 sdk-python/src/primitive/api/models/cli_logout_input.py create mode 100644 sdk-python/src/primitive/api/models/cli_logout_response_200.py create mode 100644 sdk-python/src/primitive/api/models/cli_logout_result.py create mode 100644 sdk-python/src/primitive/api/models/poll_cli_login_input.py create mode 100644 sdk-python/src/primitive/api/models/poll_cli_login_response_200.py create mode 100644 sdk-python/src/primitive/api/models/start_cli_login_input.py create mode 100644 sdk-python/src/primitive/api/models/start_cli_login_input_metadata.py create mode 100644 sdk-python/src/primitive/api/models/start_cli_login_response_201.py diff --git a/openapi/primitive-api.codegen.json b/openapi/primitive-api.codegen.json index dfe1ca6..15e5765 100644 --- a/openapi/primitive-api.codegen.json +++ b/openapi/primitive-api.codegen.json @@ -25,6 +25,10 @@ } ], "tags": [ + { + "name": "CLI", + "description": "Browser-assisted CLI authentication" + }, { "name": "Account", "description": "Manage your account settings, storage, and webhook secret" @@ -55,6 +59,258 @@ } ], "paths": { + "/cli/login/start": { + "post": { + "operationId": "startCliLogin", + "summary": "Start CLI browser login", + "description": "Starts a browser-assisted CLI login session. The response includes a\ndevice code for polling and a user code that the user approves in the\nbrowser. This endpoint does not require an API key.\n", + "tags": [ + "CLI" + ], + "security": [], + "requestBody": { + "required": false, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StartCliLoginInput" + } + } + } + }, + "responses": { + "201": { + "description": "CLI login session created", + "headers": { + "Cache-Control": { + "schema": { + "type": "string" + }, + "description": "Always `no-store`" + } + }, + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/SuccessEnvelope" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/CliLoginStartResult" + } + } + } + ] + } + } + } + }, + "400": { + "$ref": "#/components/responses/ValidationError" + }, + "429": { + "$ref": "#/components/responses/RateLimited" + } + } + } + }, + "/cli/login/poll": { + "post": { + "operationId": "pollCliLogin", + "summary": "Poll CLI browser login", + "description": "Polls a CLI login session until the browser approval either succeeds,\nis denied, expires, or is polled too quickly. The API key is generated\nonly after approval and is returned exactly once.\n", + "tags": [ + "CLI" + ], + "security": [], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PollCliLoginInput" + } + } + } + }, + "responses": { + "200": { + "description": "CLI login approved and API key created", + "headers": { + "Cache-Control": { + "schema": { + "type": "string" + }, + "description": "Always `no-store`" + } + }, + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/SuccessEnvelope" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/CliLoginPollResult" + } + } + } + ] + } + } + } + }, + "400": { + "description": "Invalid request, pending authorization, expired token, or invalid device code", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + }, + "examples": { + "authorization_pending": { + "summary": "Awaiting browser approval", + "value": { + "success": false, + "error": { + "code": "authorization_pending", + "message": "CLI login is still pending browser approval" + } + } + }, + "expired_token": { + "summary": "Login session expired", + "value": { + "success": false, + "error": { + "code": "expired_token", + "message": "CLI login code expired; run primitive login again" + } + } + }, + "invalid_device_code": { + "summary": "Unknown device code", + "value": { + "success": false, + "error": { + "code": "invalid_device_code", + "message": "Invalid CLI login device code" + } + } + } + } + } + } + }, + "403": { + "description": "CLI login was denied in the browser", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + }, + "example": { + "success": false, + "error": { + "code": "access_denied", + "message": "CLI login was denied in the browser" + } + } + } + } + }, + "429": { + "description": "Polling too quickly", + "headers": { + "Retry-After": { + "schema": { + "type": "integer" + }, + "description": "Seconds to wait before polling again" + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + }, + "example": { + "success": false, + "error": { + "code": "slow_down", + "message": "Polling too quickly; slow down and retry later" + } + } + } + } + } + } + } + }, + "/cli/logout": { + "post": { + "operationId": "cliLogout", + "summary": "Revoke the current CLI API key", + "description": "Revokes the API key used to authenticate the request. CLI clients use\nthis endpoint during `primitive logout` before removing local credentials.\n", + "tags": [ + "CLI" + ], + "requestBody": { + "required": false, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CliLogoutInput" + } + } + } + }, + "responses": { + "200": { + "description": "CLI API key revoked", + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/SuccessEnvelope" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/CliLogoutResult" + } + } + } + ] + } + } + } + }, + "400": { + "$ref": "#/components/responses/ValidationError" + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + }, + "403": { + "$ref": "#/components/responses/Forbidden" + }, + "404": { + "$ref": "#/components/responses/NotFound" + } + } + } + }, "/account": { "get": { "operationId": "getAccount", @@ -2149,7 +2405,12 @@ "outbound_response_malformed", "outbound_relay_failed", "discard_not_enabled", - "inbound_not_repliable" + "inbound_not_repliable", + "authorization_pending", + "slow_down", + "access_denied", + "expired_token", + "invalid_device_code" ] }, "message": { @@ -2287,6 +2548,133 @@ "subject" ] }, + "StartCliLoginInput": { + "type": "object", + "additionalProperties": false, + "properties": { + "device_name": { + "type": "string", + "minLength": 1, + "maxLength": 80, + "description": "Human-readable device name shown during browser approval" + }, + "metadata": { + "type": "object", + "additionalProperties": true, + "description": "Optional client metadata stored with the login session" + } + } + }, + "CliLoginStartResult": { + "type": "object", + "properties": { + "device_code": { + "type": "string", + "description": "Opaque code used by the CLI to poll for approval" + }, + "user_code": { + "type": "string", + "pattern": "^[BCDFGHJKLMNPQRSTVWXZ]{4}-[BCDFGHJKLMNPQRSTVWXZ]{4}$", + "description": "Short code the user confirms in the browser" + }, + "verification_uri": { + "type": "string", + "description": "Browser URL where the user approves the login" + }, + "verification_uri_complete": { + "type": "string", + "description": "Browser URL with the user code prefilled" + }, + "expires_in": { + "type": "integer", + "description": "Seconds until the login session expires" + }, + "interval": { + "type": "integer", + "description": "Minimum seconds between poll requests" + } + }, + "required": [ + "device_code", + "user_code", + "verification_uri", + "verification_uri_complete", + "expires_in", + "interval" + ] + }, + "PollCliLoginInput": { + "type": "object", + "additionalProperties": false, + "properties": { + "device_code": { + "type": "string", + "minLength": 1 + } + }, + "required": [ + "device_code" + ] + }, + "CliLoginPollResult": { + "type": "object", + "properties": { + "api_key": { + "type": "string", + "description": "Newly-created API key for CLI authentication" + }, + "key_id": { + "type": "string", + "format": "uuid" + }, + "key_prefix": { + "type": "string" + }, + "org_id": { + "type": "string", + "format": "uuid" + }, + "org_name": { + "type": "string", + "nullable": true + } + }, + "required": [ + "api_key", + "key_id", + "key_prefix", + "org_id", + "org_name" + ] + }, + "CliLogoutInput": { + "type": "object", + "additionalProperties": false, + "properties": { + "key_id": { + "type": "string", + "format": "uuid", + "description": "Optional key id guard; when provided it must match the authenticated API key" + } + } + }, + "CliLogoutResult": { + "type": "object", + "properties": { + "revoked": { + "type": "boolean", + "const": true + }, + "key_id": { + "type": "string", + "format": "uuid" + } + }, + "required": [ + "revoked", + "key_id" + ] + }, "Account": { "type": "object", "properties": { diff --git a/openapi/primitive-api.yaml b/openapi/primitive-api.yaml index c775f01..65da482 100644 --- a/openapi/primitive-api.yaml +++ b/openapi/primitive-api.yaml @@ -127,6 +127,8 @@ security: - BearerAuth: [] tags: + - name: CLI + description: Browser-assisted CLI authentication - name: Account description: Manage your account settings, storage, and webhook secret - name: Domains @@ -143,6 +145,171 @@ tags: description: View and replay webhook delivery attempts paths: + # --------------------------------------------------------------------------- + # CLI + # --------------------------------------------------------------------------- + /cli/login/start: + post: + operationId: startCliLogin + summary: Start CLI browser login + description: | + Starts a browser-assisted CLI login session. The response includes a + device code for polling and a user code that the user approves in the + browser. This endpoint does not require an API key. + tags: [CLI] + security: [] + requestBody: + required: false + content: + application/json: + schema: + $ref: '#/components/schemas/StartCliLoginInput' + responses: + '201': + description: CLI login session created + headers: + Cache-Control: + schema: + type: string + description: Always `no-store` + content: + application/json: + schema: + allOf: + - $ref: '#/components/schemas/SuccessEnvelope' + - type: object + properties: + data: + $ref: '#/components/schemas/CliLoginStartResult' + '400': + $ref: '#/components/responses/ValidationError' + '429': + $ref: '#/components/responses/RateLimited' + + /cli/login/poll: + post: + operationId: pollCliLogin + summary: Poll CLI browser login + description: | + Polls a CLI login session until the browser approval either succeeds, + is denied, expires, or is polled too quickly. The API key is generated + only after approval and is returned exactly once. + tags: [CLI] + security: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/PollCliLoginInput' + responses: + '200': + description: CLI login approved and API key created + headers: + Cache-Control: + schema: + type: string + description: Always `no-store` + content: + application/json: + schema: + allOf: + - $ref: '#/components/schemas/SuccessEnvelope' + - type: object + properties: + data: + $ref: '#/components/schemas/CliLoginPollResult' + '400': + description: Invalid request, pending authorization, expired token, or invalid device code + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + examples: + authorization_pending: + summary: Awaiting browser approval + value: + success: false + error: + code: authorization_pending + message: CLI login is still pending browser approval + expired_token: + summary: Login session expired + value: + success: false + error: + code: expired_token + message: CLI login code expired; run primitive login again + invalid_device_code: + summary: Unknown device code + value: + success: false + error: + code: invalid_device_code + message: Invalid CLI login device code + '403': + description: CLI login was denied in the browser + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + example: + success: false + error: + code: access_denied + message: CLI login was denied in the browser + '429': + description: Polling too quickly + headers: + Retry-After: + schema: + type: integer + description: Seconds to wait before polling again + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + example: + success: false + error: + code: slow_down + message: Polling too quickly; slow down and retry later + + /cli/logout: + post: + operationId: cliLogout + summary: Revoke the current CLI API key + description: | + Revokes the API key used to authenticate the request. CLI clients use + this endpoint during `primitive logout` before removing local credentials. + tags: [CLI] + requestBody: + required: false + content: + application/json: + schema: + $ref: '#/components/schemas/CliLogoutInput' + responses: + '200': + description: CLI API key revoked + content: + application/json: + schema: + allOf: + - $ref: '#/components/schemas/SuccessEnvelope' + - type: object + properties: + data: + $ref: '#/components/schemas/CliLogoutResult' + '400': + $ref: '#/components/responses/ValidationError' + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/Forbidden' + '404': + $ref: '#/components/responses/NotFound' + # --------------------------------------------------------------------------- # Account # --------------------------------------------------------------------------- @@ -1686,6 +1853,11 @@ components: - outbound_relay_failed - discard_not_enabled - inbound_not_repliable + - authorization_pending + - slow_down + - access_denied + - expired_token + - invalid_device_code message: type: string details: @@ -1768,6 +1940,100 @@ components: description: Entity the action applies to. required: [action, subject] + # ------------------------------------------------------------------------- + # CLI + # ------------------------------------------------------------------------- + StartCliLoginInput: + type: object + additionalProperties: false + properties: + device_name: + type: string + minLength: 1 + maxLength: 80 + description: Human-readable device name shown during browser approval + metadata: + type: object + additionalProperties: true + description: Optional client metadata stored with the login session + + CliLoginStartResult: + type: object + properties: + device_code: + type: string + description: Opaque code used by the CLI to poll for approval + user_code: + type: string + pattern: '^[BCDFGHJKLMNPQRSTVWXZ]{4}-[BCDFGHJKLMNPQRSTVWXZ]{4}$' + description: Short code the user confirms in the browser + verification_uri: + type: string + description: Browser URL where the user approves the login + verification_uri_complete: + type: string + description: Browser URL with the user code prefilled + expires_in: + type: integer + description: Seconds until the login session expires + interval: + type: integer + description: Minimum seconds between poll requests + required: + - device_code + - user_code + - verification_uri + - verification_uri_complete + - expires_in + - interval + + PollCliLoginInput: + type: object + additionalProperties: false + properties: + device_code: + type: string + minLength: 1 + required: [device_code] + + CliLoginPollResult: + type: object + properties: + api_key: + type: string + description: Newly-created API key for CLI authentication + key_id: + type: string + format: uuid + key_prefix: + type: string + org_id: + type: string + format: uuid + org_name: + type: [string, 'null'] + required: [api_key, key_id, key_prefix, org_id, org_name] + + CliLogoutInput: + type: object + additionalProperties: false + properties: + key_id: + type: string + format: uuid + description: Optional key id guard; when provided it must match the authenticated API key + + CliLogoutResult: + type: object + properties: + revoked: + type: boolean + const: true + key_id: + type: string + format: uuid + required: [revoked, key_id] + # ------------------------------------------------------------------------- # Account # ------------------------------------------------------------------------- diff --git a/sdk-go/api/oas_cfg_gen.go b/sdk-go/api/oas_cfg_gen.go index 75b8783..f4fd271 100644 --- a/sdk-go/api/oas_cfg_gen.go +++ b/sdk-go/api/oas_cfg_gen.go @@ -18,6 +18,7 @@ import ( ) var regexMap = map[string]ogenregex.Regexp{ + "^[BCDFGHJKLMNPQRSTVWXZ]{4}-[BCDFGHJKLMNPQRSTVWXZ]{4}$": ogenregex.MustCompile("^[BCDFGHJKLMNPQRSTVWXZ]{4}-[BCDFGHJKLMNPQRSTVWXZ]{4}$"), "^[\\x21-\\x7E]+$": ogenregex.MustCompile("^[\\x21-\\x7E]+$"), "^[^\\x00-\\x1F\\x7F]+$": ogenregex.MustCompile("^[^\\x00-\\x1F\\x7F]+$"), "^\\d+$": ogenregex.MustCompile("^\\d+$"), diff --git a/sdk-go/api/oas_client_gen.go b/sdk-go/api/oas_client_gen.go index f572468..a1a823f 100644 --- a/sdk-go/api/oas_client_gen.go +++ b/sdk-go/api/oas_client_gen.go @@ -36,6 +36,13 @@ type Invoker interface { // // POST /domains AddDomain(ctx context.Context, request *AddDomainInput) (AddDomainRes, error) + // CliLogout invokes cliLogout operation. + // + // Revokes the API key used to authenticate the request. CLI clients use + // this endpoint during `primitive logout` before removing local credentials. + // + // POST /cli/logout + CliLogout(ctx context.Context, request OptCliLogoutInput) (CliLogoutRes, error) // CreateEndpoint invokes createEndpoint operation. // // Creates a new webhook endpoint. If a deactivated endpoint @@ -284,6 +291,14 @@ type Invoker interface { // // GET /sent-emails ListSentEmails(ctx context.Context, params ListSentEmailsParams) (ListSentEmailsRes, error) + // PollCliLogin invokes pollCliLogin operation. + // + // Polls a CLI login session until the browser approval either succeeds, + // is denied, expires, or is polled too quickly. The API key is generated + // only after approval and is returned exactly once. + // + // POST /cli/login/poll + PollCliLogin(ctx context.Context, request *PollCliLoginInput) (PollCliLoginRes, error) // ReplayDelivery invokes replayDelivery operation. // // Re-sends the stored webhook payload from a previous delivery attempt. @@ -335,6 +350,14 @@ type Invoker interface { // // POST /send-mail SendEmail(ctx context.Context, request *SendMailInput, params SendEmailParams) (SendEmailRes, error) + // StartCliLogin invokes startCliLogin operation. + // + // Starts a browser-assisted CLI login session. The response includes a + // device code for polling and a user code that the user approves in the + // browser. This endpoint does not require an API key. + // + // POST /cli/login/start + StartCliLogin(ctx context.Context, request OptStartCliLoginInput) (StartCliLoginRes, error) // TestEndpoint invokes testEndpoint operation. // // Sends a sample `email.received` event to the endpoint. The request @@ -535,6 +558,117 @@ func (c *Client) sendAddDomain(ctx context.Context, request *AddDomainInput) (re return result, nil } +// CliLogout invokes cliLogout operation. +// +// Revokes the API key used to authenticate the request. CLI clients use +// this endpoint during `primitive logout` before removing local credentials. +// +// POST /cli/logout +func (c *Client) CliLogout(ctx context.Context, request OptCliLogoutInput) (CliLogoutRes, error) { + res, err := c.sendCliLogout(ctx, request) + return res, err +} + +func (c *Client) sendCliLogout(ctx context.Context, request OptCliLogoutInput) (res CliLogoutRes, err error) { + otelAttrs := []attribute.KeyValue{ + otelogen.OperationID("cliLogout"), + semconv.HTTPRequestMethodKey.String("POST"), + semconv.URLTemplateKey.String("/cli/logout"), + } + otelAttrs = append(otelAttrs, c.cfg.Attributes...) + + // Run stopwatch. + startTime := time.Now() + defer func() { + // Use floating point division here for higher precision (instead of Millisecond method). + elapsedDuration := time.Since(startTime) + c.duration.Record(ctx, float64(elapsedDuration)/float64(time.Millisecond), metric.WithAttributes(otelAttrs...)) + }() + + // Increment request counter. + c.requests.Add(ctx, 1, metric.WithAttributes(otelAttrs...)) + + // Start a span for this request. + ctx, span := c.cfg.Tracer.Start(ctx, CliLogoutOperation, + trace.WithAttributes(otelAttrs...), + clientSpanKind, + ) + // Track stage for error reporting. + var stage string + defer func() { + if err != nil { + span.RecordError(err) + span.SetStatus(codes.Error, stage) + c.errors.Add(ctx, 1, metric.WithAttributes(otelAttrs...)) + } + span.End() + }() + + stage = "BuildURL" + u := uri.Clone(c.requestURL(ctx)) + var pathParts [1]string + pathParts[0] = "/cli/logout" + uri.AddPathParts(u, pathParts[:]...) + + stage = "EncodeRequest" + r, err := ht.NewRequest(ctx, "POST", u) + if err != nil { + return res, errors.Wrap(err, "create request") + } + if err := encodeCliLogoutRequest(request, r); err != nil { + return res, errors.Wrap(err, "encode request") + } + + { + type bitset = [1]uint8 + var satisfied bitset + { + stage = "Security:BearerAuth" + switch err := c.securityBearerAuth(ctx, CliLogoutOperation, r); { + case err == nil: // if NO error + satisfied[0] |= 1 << 0 + case errors.Is(err, ogenerrors.ErrSkipClientSecurity): + // Skip this security. + default: + return res, errors.Wrap(err, "security \"BearerAuth\"") + } + } + + if ok := func() bool { + nextRequirement: + for _, requirement := range []bitset{ + {0b00000001}, + } { + for i, mask := range requirement { + if satisfied[i]&mask != mask { + continue nextRequirement + } + } + return true + } + return false + }(); !ok { + return res, ogenerrors.ErrSecurityRequirementIsNotSatisfied + } + } + + stage = "SendRequest" + resp, err := c.cfg.Client.Do(r) + if err != nil { + return res, errors.Wrap(err, "do request") + } + body := resp.Body + defer body.Close() + + stage = "DecodeResponse" + result, err := decodeCliLogoutResponse(resp) + if err != nil { + return res, errors.Wrap(err, "decode response") + } + + return result, nil +} + // CreateEndpoint invokes createEndpoint operation. // // Creates a new webhook endpoint. If a deactivated endpoint @@ -3493,6 +3627,85 @@ func (c *Client) sendListSentEmails(ctx context.Context, params ListSentEmailsPa return result, nil } +// PollCliLogin invokes pollCliLogin operation. +// +// Polls a CLI login session until the browser approval either succeeds, +// is denied, expires, or is polled too quickly. The API key is generated +// only after approval and is returned exactly once. +// +// POST /cli/login/poll +func (c *Client) PollCliLogin(ctx context.Context, request *PollCliLoginInput) (PollCliLoginRes, error) { + res, err := c.sendPollCliLogin(ctx, request) + return res, err +} + +func (c *Client) sendPollCliLogin(ctx context.Context, request *PollCliLoginInput) (res PollCliLoginRes, err error) { + otelAttrs := []attribute.KeyValue{ + otelogen.OperationID("pollCliLogin"), + semconv.HTTPRequestMethodKey.String("POST"), + semconv.URLTemplateKey.String("/cli/login/poll"), + } + otelAttrs = append(otelAttrs, c.cfg.Attributes...) + + // Run stopwatch. + startTime := time.Now() + defer func() { + // Use floating point division here for higher precision (instead of Millisecond method). + elapsedDuration := time.Since(startTime) + c.duration.Record(ctx, float64(elapsedDuration)/float64(time.Millisecond), metric.WithAttributes(otelAttrs...)) + }() + + // Increment request counter. + c.requests.Add(ctx, 1, metric.WithAttributes(otelAttrs...)) + + // Start a span for this request. + ctx, span := c.cfg.Tracer.Start(ctx, PollCliLoginOperation, + trace.WithAttributes(otelAttrs...), + clientSpanKind, + ) + // Track stage for error reporting. + var stage string + defer func() { + if err != nil { + span.RecordError(err) + span.SetStatus(codes.Error, stage) + c.errors.Add(ctx, 1, metric.WithAttributes(otelAttrs...)) + } + span.End() + }() + + stage = "BuildURL" + u := uri.Clone(c.requestURL(ctx)) + var pathParts [1]string + pathParts[0] = "/cli/login/poll" + uri.AddPathParts(u, pathParts[:]...) + + stage = "EncodeRequest" + r, err := ht.NewRequest(ctx, "POST", u) + if err != nil { + return res, errors.Wrap(err, "create request") + } + if err := encodePollCliLoginRequest(request, r); err != nil { + return res, errors.Wrap(err, "encode request") + } + + stage = "SendRequest" + resp, err := c.cfg.Client.Do(r) + if err != nil { + return res, errors.Wrap(err, "do request") + } + body := resp.Body + defer body.Close() + + stage = "DecodeResponse" + result, err := decodePollCliLoginResponse(resp) + if err != nil { + return res, errors.Wrap(err, "decode response") + } + + return result, nil +} + // ReplayDelivery invokes replayDelivery operation. // // Re-sends the stored webhook payload from a previous delivery attempt. @@ -4129,6 +4342,85 @@ func (c *Client) sendSendEmail(ctx context.Context, request *SendMailInput, para return result, nil } +// StartCliLogin invokes startCliLogin operation. +// +// Starts a browser-assisted CLI login session. The response includes a +// device code for polling and a user code that the user approves in the +// browser. This endpoint does not require an API key. +// +// POST /cli/login/start +func (c *Client) StartCliLogin(ctx context.Context, request OptStartCliLoginInput) (StartCliLoginRes, error) { + res, err := c.sendStartCliLogin(ctx, request) + return res, err +} + +func (c *Client) sendStartCliLogin(ctx context.Context, request OptStartCliLoginInput) (res StartCliLoginRes, err error) { + otelAttrs := []attribute.KeyValue{ + otelogen.OperationID("startCliLogin"), + semconv.HTTPRequestMethodKey.String("POST"), + semconv.URLTemplateKey.String("/cli/login/start"), + } + otelAttrs = append(otelAttrs, c.cfg.Attributes...) + + // Run stopwatch. + startTime := time.Now() + defer func() { + // Use floating point division here for higher precision (instead of Millisecond method). + elapsedDuration := time.Since(startTime) + c.duration.Record(ctx, float64(elapsedDuration)/float64(time.Millisecond), metric.WithAttributes(otelAttrs...)) + }() + + // Increment request counter. + c.requests.Add(ctx, 1, metric.WithAttributes(otelAttrs...)) + + // Start a span for this request. + ctx, span := c.cfg.Tracer.Start(ctx, StartCliLoginOperation, + trace.WithAttributes(otelAttrs...), + clientSpanKind, + ) + // Track stage for error reporting. + var stage string + defer func() { + if err != nil { + span.RecordError(err) + span.SetStatus(codes.Error, stage) + c.errors.Add(ctx, 1, metric.WithAttributes(otelAttrs...)) + } + span.End() + }() + + stage = "BuildURL" + u := uri.Clone(c.requestURL(ctx)) + var pathParts [1]string + pathParts[0] = "/cli/login/start" + uri.AddPathParts(u, pathParts[:]...) + + stage = "EncodeRequest" + r, err := ht.NewRequest(ctx, "POST", u) + if err != nil { + return res, errors.Wrap(err, "create request") + } + if err := encodeStartCliLoginRequest(request, r); err != nil { + return res, errors.Wrap(err, "encode request") + } + + stage = "SendRequest" + resp, err := c.cfg.Client.Do(r) + if err != nil { + return res, errors.Wrap(err, "do request") + } + body := resp.Body + defer body.Close() + + stage = "DecodeResponse" + result, err := decodeStartCliLoginResponse(resp) + if err != nil { + return res, errors.Wrap(err, "decode response") + } + + return result, nil +} + // TestEndpoint invokes testEndpoint operation. // // Sends a sample `email.received` event to the endpoint. The request diff --git a/sdk-go/api/oas_handlers_gen.go b/sdk-go/api/oas_handlers_gen.go index 1c069f8..3c16a8f 100644 --- a/sdk-go/api/oas_handlers_gen.go +++ b/sdk-go/api/oas_handlers_gen.go @@ -222,6 +222,194 @@ func (s *Server) handleAddDomainRequest(args [0]string, argsEscaped bool, w http } } +// handleCliLogoutRequest handles cliLogout operation. +// +// Revokes the API key used to authenticate the request. CLI clients use +// this endpoint during `primitive logout` before removing local credentials. +// +// POST /cli/logout +func (s *Server) handleCliLogoutRequest(args [0]string, argsEscaped bool, w http.ResponseWriter, r *http.Request) { + statusWriter := &codeRecorder{ResponseWriter: w} + w = statusWriter + otelAttrs := []attribute.KeyValue{ + otelogen.OperationID("cliLogout"), + semconv.HTTPRequestMethodKey.String("POST"), + semconv.HTTPRouteKey.String("/cli/logout"), + } + // Add attributes from config. + otelAttrs = append(otelAttrs, s.cfg.Attributes...) + + // Start a span for this request. + ctx, span := s.cfg.Tracer.Start(r.Context(), CliLogoutOperation, + trace.WithAttributes(otelAttrs...), + serverSpanKind, + ) + defer span.End() + + // Add Labeler to context. + labeler := &Labeler{attrs: otelAttrs} + ctx = contextWithLabeler(ctx, labeler) + + // Run stopwatch. + startTime := time.Now() + defer func() { + elapsedDuration := time.Since(startTime) + + attrSet := labeler.AttributeSet() + attrs := attrSet.ToSlice() + code := statusWriter.status + if code != 0 { + codeAttr := semconv.HTTPResponseStatusCode(code) + attrs = append(attrs, codeAttr) + span.SetAttributes(codeAttr) + } + attrOpt := metric.WithAttributes(attrs...) + + // Increment request counter. + s.requests.Add(ctx, 1, attrOpt) + + // Use floating point division here for higher precision (instead of Millisecond method). + s.duration.Record(ctx, float64(elapsedDuration)/float64(time.Millisecond), attrOpt) + }() + + var ( + recordError = func(stage string, err error) { + span.RecordError(err) + + // https://opentelemetry.io/docs/specs/semconv/http/http-spans/#status + // Span Status MUST be left unset if HTTP status code was in the 1xx, 2xx or 3xx ranges, + // unless there was another error (e.g., network error receiving the response body; or 3xx codes with + // max redirects exceeded), in which case status MUST be set to Error. + code := statusWriter.status + if code < 100 || code >= 500 { + span.SetStatus(codes.Error, stage) + } + + attrSet := labeler.AttributeSet() + attrs := attrSet.ToSlice() + if code != 0 { + attrs = append(attrs, semconv.HTTPResponseStatusCode(code)) + } + + s.errors.Add(ctx, 1, metric.WithAttributes(attrs...)) + } + err error + opErrContext = ogenerrors.OperationContext{ + Name: CliLogoutOperation, + ID: "cliLogout", + } + ) + { + type bitset = [1]uint8 + var satisfied bitset + { + sctx, ok, err := s.securityBearerAuth(ctx, CliLogoutOperation, r) + if err != nil { + err = &ogenerrors.SecurityError{ + OperationContext: opErrContext, + Security: "BearerAuth", + Err: err, + } + defer recordError("Security:BearerAuth", err) + s.cfg.ErrorHandler(ctx, w, r, err) + return + } + if ok { + satisfied[0] |= 1 << 0 + ctx = sctx + } + } + + if ok := func() bool { + nextRequirement: + for _, requirement := range []bitset{ + {0b00000001}, + } { + for i, mask := range requirement { + if satisfied[i]&mask != mask { + continue nextRequirement + } + } + return true + } + return false + }(); !ok { + err = &ogenerrors.SecurityError{ + OperationContext: opErrContext, + Err: ogenerrors.ErrSecurityRequirementIsNotSatisfied, + } + defer recordError("Security", err) + s.cfg.ErrorHandler(ctx, w, r, err) + return + } + } + + var rawBody []byte + request, rawBody, close, err := s.decodeCliLogoutRequest(r) + if err != nil { + err = &ogenerrors.DecodeRequestError{ + OperationContext: opErrContext, + Err: err, + } + defer recordError("DecodeRequest", err) + s.cfg.ErrorHandler(ctx, w, r, err) + return + } + defer func() { + if err := close(); err != nil { + recordError("CloseRequest", err) + } + }() + + var response CliLogoutRes + if m := s.cfg.Middleware; m != nil { + mreq := middleware.Request{ + Context: ctx, + OperationName: CliLogoutOperation, + OperationSummary: "Revoke the current CLI API key", + OperationID: "cliLogout", + Body: request, + RawBody: rawBody, + Params: middleware.Parameters{}, + Raw: r, + } + + type ( + Request = OptCliLogoutInput + Params = struct{} + Response = CliLogoutRes + ) + response, err = middleware.HookMiddleware[ + Request, + Params, + Response, + ]( + m, + mreq, + nil, + func(ctx context.Context, request Request, params Params) (response Response, err error) { + response, err = s.h.CliLogout(ctx, request) + return response, err + }, + ) + } else { + response, err = s.h.CliLogout(ctx, request) + } + if err != nil { + defer recordError("Internal", err) + s.cfg.ErrorHandler(ctx, w, r, err) + return + } + + if err := encodeCliLogoutResponse(response, w, span); err != nil { + defer recordError("EncodeResponse", err) + if !errors.Is(err, ht.ErrInternalServerErrorResponse) { + s.cfg.ErrorHandler(ctx, w, r, err) + } + return + } +} + // handleCreateEndpointRequest handles createEndpoint operation. // // Creates a new webhook endpoint. If a deactivated endpoint @@ -4278,6 +4466,151 @@ func (s *Server) handleListSentEmailsRequest(args [0]string, argsEscaped bool, w } } +// handlePollCliLoginRequest handles pollCliLogin operation. +// +// Polls a CLI login session until the browser approval either succeeds, +// is denied, expires, or is polled too quickly. The API key is generated +// only after approval and is returned exactly once. +// +// POST /cli/login/poll +func (s *Server) handlePollCliLoginRequest(args [0]string, argsEscaped bool, w http.ResponseWriter, r *http.Request) { + statusWriter := &codeRecorder{ResponseWriter: w} + w = statusWriter + otelAttrs := []attribute.KeyValue{ + otelogen.OperationID("pollCliLogin"), + semconv.HTTPRequestMethodKey.String("POST"), + semconv.HTTPRouteKey.String("/cli/login/poll"), + } + // Add attributes from config. + otelAttrs = append(otelAttrs, s.cfg.Attributes...) + + // Start a span for this request. + ctx, span := s.cfg.Tracer.Start(r.Context(), PollCliLoginOperation, + trace.WithAttributes(otelAttrs...), + serverSpanKind, + ) + defer span.End() + + // Add Labeler to context. + labeler := &Labeler{attrs: otelAttrs} + ctx = contextWithLabeler(ctx, labeler) + + // Run stopwatch. + startTime := time.Now() + defer func() { + elapsedDuration := time.Since(startTime) + + attrSet := labeler.AttributeSet() + attrs := attrSet.ToSlice() + code := statusWriter.status + if code != 0 { + codeAttr := semconv.HTTPResponseStatusCode(code) + attrs = append(attrs, codeAttr) + span.SetAttributes(codeAttr) + } + attrOpt := metric.WithAttributes(attrs...) + + // Increment request counter. + s.requests.Add(ctx, 1, attrOpt) + + // Use floating point division here for higher precision (instead of Millisecond method). + s.duration.Record(ctx, float64(elapsedDuration)/float64(time.Millisecond), attrOpt) + }() + + var ( + recordError = func(stage string, err error) { + span.RecordError(err) + + // https://opentelemetry.io/docs/specs/semconv/http/http-spans/#status + // Span Status MUST be left unset if HTTP status code was in the 1xx, 2xx or 3xx ranges, + // unless there was another error (e.g., network error receiving the response body; or 3xx codes with + // max redirects exceeded), in which case status MUST be set to Error. + code := statusWriter.status + if code < 100 || code >= 500 { + span.SetStatus(codes.Error, stage) + } + + attrSet := labeler.AttributeSet() + attrs := attrSet.ToSlice() + if code != 0 { + attrs = append(attrs, semconv.HTTPResponseStatusCode(code)) + } + + s.errors.Add(ctx, 1, metric.WithAttributes(attrs...)) + } + err error + opErrContext = ogenerrors.OperationContext{ + Name: PollCliLoginOperation, + ID: "pollCliLogin", + } + ) + + var rawBody []byte + request, rawBody, close, err := s.decodePollCliLoginRequest(r) + if err != nil { + err = &ogenerrors.DecodeRequestError{ + OperationContext: opErrContext, + Err: err, + } + defer recordError("DecodeRequest", err) + s.cfg.ErrorHandler(ctx, w, r, err) + return + } + defer func() { + if err := close(); err != nil { + recordError("CloseRequest", err) + } + }() + + var response PollCliLoginRes + if m := s.cfg.Middleware; m != nil { + mreq := middleware.Request{ + Context: ctx, + OperationName: PollCliLoginOperation, + OperationSummary: "Poll CLI browser login", + OperationID: "pollCliLogin", + Body: request, + RawBody: rawBody, + Params: middleware.Parameters{}, + Raw: r, + } + + type ( + Request = *PollCliLoginInput + Params = struct{} + Response = PollCliLoginRes + ) + response, err = middleware.HookMiddleware[ + Request, + Params, + Response, + ]( + m, + mreq, + nil, + func(ctx context.Context, request Request, params Params) (response Response, err error) { + response, err = s.h.PollCliLogin(ctx, request) + return response, err + }, + ) + } else { + response, err = s.h.PollCliLogin(ctx, request) + } + if err != nil { + defer recordError("Internal", err) + s.cfg.ErrorHandler(ctx, w, r, err) + return + } + + if err := encodePollCliLoginResponse(response, w, span); err != nil { + defer recordError("EncodeResponse", err) + if !errors.Is(err, ht.ErrInternalServerErrorResponse) { + s.cfg.ErrorHandler(ctx, w, r, err) + } + return + } +} + // handleReplayDeliveryRequest handles replayDelivery operation. // // Re-sends the stored webhook payload from a previous delivery attempt. @@ -5249,6 +5582,151 @@ func (s *Server) handleSendEmailRequest(args [0]string, argsEscaped bool, w http } } +// handleStartCliLoginRequest handles startCliLogin operation. +// +// Starts a browser-assisted CLI login session. The response includes a +// device code for polling and a user code that the user approves in the +// browser. This endpoint does not require an API key. +// +// POST /cli/login/start +func (s *Server) handleStartCliLoginRequest(args [0]string, argsEscaped bool, w http.ResponseWriter, r *http.Request) { + statusWriter := &codeRecorder{ResponseWriter: w} + w = statusWriter + otelAttrs := []attribute.KeyValue{ + otelogen.OperationID("startCliLogin"), + semconv.HTTPRequestMethodKey.String("POST"), + semconv.HTTPRouteKey.String("/cli/login/start"), + } + // Add attributes from config. + otelAttrs = append(otelAttrs, s.cfg.Attributes...) + + // Start a span for this request. + ctx, span := s.cfg.Tracer.Start(r.Context(), StartCliLoginOperation, + trace.WithAttributes(otelAttrs...), + serverSpanKind, + ) + defer span.End() + + // Add Labeler to context. + labeler := &Labeler{attrs: otelAttrs} + ctx = contextWithLabeler(ctx, labeler) + + // Run stopwatch. + startTime := time.Now() + defer func() { + elapsedDuration := time.Since(startTime) + + attrSet := labeler.AttributeSet() + attrs := attrSet.ToSlice() + code := statusWriter.status + if code != 0 { + codeAttr := semconv.HTTPResponseStatusCode(code) + attrs = append(attrs, codeAttr) + span.SetAttributes(codeAttr) + } + attrOpt := metric.WithAttributes(attrs...) + + // Increment request counter. + s.requests.Add(ctx, 1, attrOpt) + + // Use floating point division here for higher precision (instead of Millisecond method). + s.duration.Record(ctx, float64(elapsedDuration)/float64(time.Millisecond), attrOpt) + }() + + var ( + recordError = func(stage string, err error) { + span.RecordError(err) + + // https://opentelemetry.io/docs/specs/semconv/http/http-spans/#status + // Span Status MUST be left unset if HTTP status code was in the 1xx, 2xx or 3xx ranges, + // unless there was another error (e.g., network error receiving the response body; or 3xx codes with + // max redirects exceeded), in which case status MUST be set to Error. + code := statusWriter.status + if code < 100 || code >= 500 { + span.SetStatus(codes.Error, stage) + } + + attrSet := labeler.AttributeSet() + attrs := attrSet.ToSlice() + if code != 0 { + attrs = append(attrs, semconv.HTTPResponseStatusCode(code)) + } + + s.errors.Add(ctx, 1, metric.WithAttributes(attrs...)) + } + err error + opErrContext = ogenerrors.OperationContext{ + Name: StartCliLoginOperation, + ID: "startCliLogin", + } + ) + + var rawBody []byte + request, rawBody, close, err := s.decodeStartCliLoginRequest(r) + if err != nil { + err = &ogenerrors.DecodeRequestError{ + OperationContext: opErrContext, + Err: err, + } + defer recordError("DecodeRequest", err) + s.cfg.ErrorHandler(ctx, w, r, err) + return + } + defer func() { + if err := close(); err != nil { + recordError("CloseRequest", err) + } + }() + + var response StartCliLoginRes + if m := s.cfg.Middleware; m != nil { + mreq := middleware.Request{ + Context: ctx, + OperationName: StartCliLoginOperation, + OperationSummary: "Start CLI browser login", + OperationID: "startCliLogin", + Body: request, + RawBody: rawBody, + Params: middleware.Parameters{}, + Raw: r, + } + + type ( + Request = OptStartCliLoginInput + Params = struct{} + Response = StartCliLoginRes + ) + response, err = middleware.HookMiddleware[ + Request, + Params, + Response, + ]( + m, + mreq, + nil, + func(ctx context.Context, request Request, params Params) (response Response, err error) { + response, err = s.h.StartCliLogin(ctx, request) + return response, err + }, + ) + } else { + response, err = s.h.StartCliLogin(ctx, request) + } + if err != nil { + defer recordError("Internal", err) + s.cfg.ErrorHandler(ctx, w, r, err) + return + } + + if err := encodeStartCliLoginResponse(response, w, span); err != nil { + defer recordError("EncodeResponse", err) + if !errors.Is(err, ht.ErrInternalServerErrorResponse) { + s.cfg.ErrorHandler(ctx, w, r, err) + } + return + } +} + // handleTestEndpointRequest handles testEndpoint operation. // // Sends a sample `email.received` event to the endpoint. The request diff --git a/sdk-go/api/oas_interfaces_gen.go b/sdk-go/api/oas_interfaces_gen.go index 4145a5e..5e70889 100644 --- a/sdk-go/api/oas_interfaces_gen.go +++ b/sdk-go/api/oas_interfaces_gen.go @@ -5,6 +5,10 @@ type AddDomainRes interface { addDomainRes() } +type CliLogoutRes interface { + cliLogoutRes() +} + type CreateEndpointRes interface { createEndpointRes() } @@ -89,6 +93,10 @@ type ListSentEmailsRes interface { listSentEmailsRes() } +type PollCliLoginRes interface { + pollCliLoginRes() +} + type ReplayDeliveryRes interface { replayDeliveryRes() } @@ -109,6 +117,10 @@ type SendEmailRes interface { sendEmailRes() } +type StartCliLoginRes interface { + startCliLoginRes() +} + type TestEndpointRes interface { testEndpointRes() } diff --git a/sdk-go/api/oas_json_gen.go b/sdk-go/api/oas_json_gen.go index b232862..3a856e9 100644 --- a/sdk-go/api/oas_json_gen.go +++ b/sdk-go/api/oas_json_gen.go @@ -783,110 +783,123 @@ func (s *AddDomainUnauthorized) UnmarshalJSON(data []byte) error { return s.Decode(d) } -// Encode encodes CreateEndpointBadRequest as json. -func (s *CreateEndpointBadRequest) Encode(e *jx.Encoder) { - unwrapped := (*ErrorResponse)(s) - - unwrapped.Encode(e) -} - -// Decode decodes CreateEndpointBadRequest from json. -func (s *CreateEndpointBadRequest) Decode(d *jx.Decoder) error { - if s == nil { - return errors.New("invalid: unable to decode CreateEndpointBadRequest to nil") - } - var unwrapped ErrorResponse - if err := func() error { - if err := unwrapped.Decode(d); err != nil { - return err - } - return nil - }(); err != nil { - return errors.Wrap(err, "alias") - } - *s = CreateEndpointBadRequest(unwrapped) - return nil -} - -// MarshalJSON implements stdjson.Marshaler. -func (s *CreateEndpointBadRequest) MarshalJSON() ([]byte, error) { - e := jx.Encoder{} - s.Encode(&e) - return e.Bytes(), nil -} - -// UnmarshalJSON implements stdjson.Unmarshaler. -func (s *CreateEndpointBadRequest) UnmarshalJSON(data []byte) error { - d := jx.DecodeBytes(data) - return s.Decode(d) -} - // Encode implements json.Marshaler. -func (s *CreateEndpointCreated) Encode(e *jx.Encoder) { +func (s *CliLoginPollResult) Encode(e *jx.Encoder) { e.ObjStart() s.encodeFields(e) e.ObjEnd() } // encodeFields encodes fields. -func (s *CreateEndpointCreated) encodeFields(e *jx.Encoder) { +func (s *CliLoginPollResult) encodeFields(e *jx.Encoder) { { - e.FieldStart("success") - e.Bool(true) + e.FieldStart("api_key") + e.Str(s.APIKey) } { - e.FieldStart("data") - s.Data.Encode(e) + e.FieldStart("key_id") + json.EncodeUUID(e, s.KeyID) + } + { + e.FieldStart("key_prefix") + e.Str(s.KeyPrefix) + } + { + e.FieldStart("org_id") + json.EncodeUUID(e, s.OrgID) + } + { + e.FieldStart("org_name") + s.OrgName.Encode(e) } } -var jsonFieldsNameOfCreateEndpointCreated = [2]string{ - 0: "success", - 1: "data", +var jsonFieldsNameOfCliLoginPollResult = [5]string{ + 0: "api_key", + 1: "key_id", + 2: "key_prefix", + 3: "org_id", + 4: "org_name", } -// Decode decodes CreateEndpointCreated from json. -func (s *CreateEndpointCreated) Decode(d *jx.Decoder) error { +// Decode decodes CliLoginPollResult from json. +func (s *CliLoginPollResult) Decode(d *jx.Decoder) error { if s == nil { - return errors.New("invalid: unable to decode CreateEndpointCreated to nil") + return errors.New("invalid: unable to decode CliLoginPollResult to nil") } var requiredBitSet [1]uint8 if err := d.ObjBytes(func(d *jx.Decoder, k []byte) error { switch string(k) { - case "success": + case "api_key": requiredBitSet[0] |= 1 << 0 if err := func() error { - v, err := d.Bool() - s.Success = bool(v) + v, err := d.Str() + s.APIKey = string(v) if err != nil { return err } return nil }(); err != nil { - return errors.Wrap(err, "decode field \"success\"") + return errors.Wrap(err, "decode field \"api_key\"") } - case "data": + case "key_id": requiredBitSet[0] |= 1 << 1 if err := func() error { - if err := s.Data.Decode(d); err != nil { + v, err := json.DecodeUUID(d) + s.KeyID = v + if err != nil { return err } return nil }(); err != nil { - return errors.Wrap(err, "decode field \"data\"") + return errors.Wrap(err, "decode field \"key_id\"") + } + case "key_prefix": + requiredBitSet[0] |= 1 << 2 + if err := func() error { + v, err := d.Str() + s.KeyPrefix = string(v) + if err != nil { + return err + } + return nil + }(); err != nil { + return errors.Wrap(err, "decode field \"key_prefix\"") + } + case "org_id": + requiredBitSet[0] |= 1 << 3 + if err := func() error { + v, err := json.DecodeUUID(d) + s.OrgID = v + if err != nil { + return err + } + return nil + }(); err != nil { + return errors.Wrap(err, "decode field \"org_id\"") + } + case "org_name": + requiredBitSet[0] |= 1 << 4 + if err := func() error { + if err := s.OrgName.Decode(d); err != nil { + return err + } + return nil + }(); err != nil { + return errors.Wrap(err, "decode field \"org_name\"") } default: return d.Skip() } return nil }); err != nil { - return errors.Wrap(err, "decode CreateEndpointCreated") + return errors.Wrap(err, "decode CliLoginPollResult") } // Validate required fields. var failures []validate.FieldError for i, mask := range [1]uint8{ - 0b00000011, + 0b00011111, } { if result := (requiredBitSet[i] & mask) ^ mask; result != 0 { // Mask only required fields and check equality to mask using XOR. @@ -898,8 +911,8 @@ func (s *CreateEndpointCreated) Decode(d *jx.Decoder) error { bitIdx := bits.TrailingZeros8(result) fieldIdx := i*8 + bitIdx var name string - if fieldIdx < len(jsonFieldsNameOfCreateEndpointCreated) { - name = jsonFieldsNameOfCreateEndpointCreated[fieldIdx] + if fieldIdx < len(jsonFieldsNameOfCliLoginPollResult) { + name = jsonFieldsNameOfCliLoginPollResult[fieldIdx] } else { name = strconv.Itoa(fieldIdx) } @@ -920,123 +933,154 @@ func (s *CreateEndpointCreated) Decode(d *jx.Decoder) error { } // MarshalJSON implements stdjson.Marshaler. -func (s *CreateEndpointCreated) MarshalJSON() ([]byte, error) { +func (s *CliLoginPollResult) MarshalJSON() ([]byte, error) { e := jx.Encoder{} s.Encode(&e) return e.Bytes(), nil } // UnmarshalJSON implements stdjson.Unmarshaler. -func (s *CreateEndpointCreated) UnmarshalJSON(data []byte) error { +func (s *CliLoginPollResult) UnmarshalJSON(data []byte) error { d := jx.DecodeBytes(data) return s.Decode(d) } // Encode implements json.Marshaler. -func (s *CreateEndpointInput) Encode(e *jx.Encoder) { +func (s *CliLoginStartResult) Encode(e *jx.Encoder) { e.ObjStart() s.encodeFields(e) e.ObjEnd() } // encodeFields encodes fields. -func (s *CreateEndpointInput) encodeFields(e *jx.Encoder) { +func (s *CliLoginStartResult) encodeFields(e *jx.Encoder) { { - e.FieldStart("url") - e.Str(s.URL) + e.FieldStart("device_code") + e.Str(s.DeviceCode) } { - if s.Enabled.Set { - e.FieldStart("enabled") - s.Enabled.Encode(e) - } + e.FieldStart("user_code") + e.Str(s.UserCode) } { - if s.DomainID.Set { - e.FieldStart("domain_id") - s.DomainID.Encode(e) - } + e.FieldStart("verification_uri") + json.EncodeURI(e, s.VerificationURI) } { - if s.Rules != nil { - e.FieldStart("rules") - s.Rules.Encode(e) - } + e.FieldStart("verification_uri_complete") + json.EncodeURI(e, s.VerificationURIComplete) + } + { + e.FieldStart("expires_in") + e.Int(s.ExpiresIn) + } + { + e.FieldStart("interval") + e.Int(s.Interval) } } -var jsonFieldsNameOfCreateEndpointInput = [4]string{ - 0: "url", - 1: "enabled", - 2: "domain_id", - 3: "rules", +var jsonFieldsNameOfCliLoginStartResult = [6]string{ + 0: "device_code", + 1: "user_code", + 2: "verification_uri", + 3: "verification_uri_complete", + 4: "expires_in", + 5: "interval", } -// Decode decodes CreateEndpointInput from json. -func (s *CreateEndpointInput) Decode(d *jx.Decoder) error { +// Decode decodes CliLoginStartResult from json. +func (s *CliLoginStartResult) Decode(d *jx.Decoder) error { if s == nil { - return errors.New("invalid: unable to decode CreateEndpointInput to nil") + return errors.New("invalid: unable to decode CliLoginStartResult to nil") } var requiredBitSet [1]uint8 - s.setDefaults() if err := d.ObjBytes(func(d *jx.Decoder, k []byte) error { switch string(k) { - case "url": + case "device_code": requiredBitSet[0] |= 1 << 0 if err := func() error { v, err := d.Str() - s.URL = string(v) + s.DeviceCode = string(v) if err != nil { return err } return nil }(); err != nil { - return errors.Wrap(err, "decode field \"url\"") + return errors.Wrap(err, "decode field \"device_code\"") } - case "enabled": + case "user_code": + requiredBitSet[0] |= 1 << 1 if err := func() error { - s.Enabled.Reset() - if err := s.Enabled.Decode(d); err != nil { + v, err := d.Str() + s.UserCode = string(v) + if err != nil { return err } return nil }(); err != nil { - return errors.Wrap(err, "decode field \"enabled\"") + return errors.Wrap(err, "decode field \"user_code\"") } - case "domain_id": + case "verification_uri": + requiredBitSet[0] |= 1 << 2 if err := func() error { - s.DomainID.Reset() - if err := s.DomainID.Decode(d); err != nil { + v, err := json.DecodeURI(d) + s.VerificationURI = v + if err != nil { return err } return nil }(); err != nil { - return errors.Wrap(err, "decode field \"domain_id\"") + return errors.Wrap(err, "decode field \"verification_uri\"") } - case "rules": + case "verification_uri_complete": + requiredBitSet[0] |= 1 << 3 if err := func() error { - s.Rules = nil - var elem CreateEndpointInputRules - if err := elem.Decode(d); err != nil { + v, err := json.DecodeURI(d) + s.VerificationURIComplete = v + if err != nil { return err } - s.Rules = &elem return nil }(); err != nil { - return errors.Wrap(err, "decode field \"rules\"") + return errors.Wrap(err, "decode field \"verification_uri_complete\"") + } + case "expires_in": + requiredBitSet[0] |= 1 << 4 + if err := func() error { + v, err := d.Int() + s.ExpiresIn = int(v) + if err != nil { + return err + } + return nil + }(); err != nil { + return errors.Wrap(err, "decode field \"expires_in\"") + } + case "interval": + requiredBitSet[0] |= 1 << 5 + if err := func() error { + v, err := d.Int() + s.Interval = int(v) + if err != nil { + return err + } + return nil + }(); err != nil { + return errors.Wrap(err, "decode field \"interval\"") } default: - return errors.Errorf("unexpected field %q", k) + return d.Skip() } return nil }); err != nil { - return errors.Wrap(err, "decode CreateEndpointInput") + return errors.Wrap(err, "decode CliLoginStartResult") } // Validate required fields. var failures []validate.FieldError for i, mask := range [1]uint8{ - 0b00000001, + 0b00111111, } { if result := (requiredBitSet[i] & mask) ^ mask; result != 0 { // Mask only required fields and check equality to mask using XOR. @@ -1048,8 +1092,8 @@ func (s *CreateEndpointInput) Decode(d *jx.Decoder) error { bitIdx := bits.TrailingZeros8(result) fieldIdx := i*8 + bitIdx var name string - if fieldIdx < len(jsonFieldsNameOfCreateEndpointInput) { - name = jsonFieldsNameOfCreateEndpointInput[fieldIdx] + if fieldIdx < len(jsonFieldsNameOfCliLoginStartResult) { + name = jsonFieldsNameOfCliLoginStartResult[fieldIdx] } else { name = strconv.Itoa(fieldIdx) } @@ -1070,73 +1114,29 @@ func (s *CreateEndpointInput) Decode(d *jx.Decoder) error { } // MarshalJSON implements stdjson.Marshaler. -func (s *CreateEndpointInput) MarshalJSON() ([]byte, error) { - e := jx.Encoder{} - s.Encode(&e) - return e.Bytes(), nil -} - -// UnmarshalJSON implements stdjson.Unmarshaler. -func (s *CreateEndpointInput) UnmarshalJSON(data []byte) error { - d := jx.DecodeBytes(data) - return s.Decode(d) -} - -// Encode implements json.Marshaler. -func (s *CreateEndpointInputRules) Encode(e *jx.Encoder) { - e.ObjStart() - s.encodeFields(e) - e.ObjEnd() -} - -// encodeFields encodes fields. -func (s *CreateEndpointInputRules) encodeFields(e *jx.Encoder) { -} - -var jsonFieldsNameOfCreateEndpointInputRules = [0]string{} - -// Decode decodes CreateEndpointInputRules from json. -func (s *CreateEndpointInputRules) Decode(d *jx.Decoder) error { - if s == nil { - return errors.New("invalid: unable to decode CreateEndpointInputRules to nil") - } - - if err := d.ObjBytes(func(d *jx.Decoder, k []byte) error { - switch string(k) { - default: - return d.Skip() - } - }); err != nil { - return errors.Wrap(err, "decode CreateEndpointInputRules") - } - - return nil -} - -// MarshalJSON implements stdjson.Marshaler. -func (s *CreateEndpointInputRules) MarshalJSON() ([]byte, error) { +func (s *CliLoginStartResult) MarshalJSON() ([]byte, error) { e := jx.Encoder{} s.Encode(&e) return e.Bytes(), nil } // UnmarshalJSON implements stdjson.Unmarshaler. -func (s *CreateEndpointInputRules) UnmarshalJSON(data []byte) error { +func (s *CliLoginStartResult) UnmarshalJSON(data []byte) error { d := jx.DecodeBytes(data) return s.Decode(d) } -// Encode encodes CreateEndpointUnauthorized as json. -func (s *CreateEndpointUnauthorized) Encode(e *jx.Encoder) { +// Encode encodes CliLogoutBadRequest as json. +func (s *CliLogoutBadRequest) Encode(e *jx.Encoder) { unwrapped := (*ErrorResponse)(s) unwrapped.Encode(e) } -// Decode decodes CreateEndpointUnauthorized from json. -func (s *CreateEndpointUnauthorized) Decode(d *jx.Decoder) error { +// Decode decodes CliLogoutBadRequest from json. +func (s *CliLogoutBadRequest) Decode(d *jx.Decoder) error { if s == nil { - return errors.New("invalid: unable to decode CreateEndpointUnauthorized to nil") + return errors.New("invalid: unable to decode CliLogoutBadRequest to nil") } var unwrapped ErrorResponse if err := func() error { @@ -1147,34 +1147,34 @@ func (s *CreateEndpointUnauthorized) Decode(d *jx.Decoder) error { }(); err != nil { return errors.Wrap(err, "alias") } - *s = CreateEndpointUnauthorized(unwrapped) + *s = CliLogoutBadRequest(unwrapped) return nil } // MarshalJSON implements stdjson.Marshaler. -func (s *CreateEndpointUnauthorized) MarshalJSON() ([]byte, error) { +func (s *CliLogoutBadRequest) MarshalJSON() ([]byte, error) { e := jx.Encoder{} s.Encode(&e) return e.Bytes(), nil } // UnmarshalJSON implements stdjson.Unmarshaler. -func (s *CreateEndpointUnauthorized) UnmarshalJSON(data []byte) error { +func (s *CliLogoutBadRequest) UnmarshalJSON(data []byte) error { d := jx.DecodeBytes(data) return s.Decode(d) } -// Encode encodes CreateFilterBadRequest as json. -func (s *CreateFilterBadRequest) Encode(e *jx.Encoder) { +// Encode encodes CliLogoutForbidden as json. +func (s *CliLogoutForbidden) Encode(e *jx.Encoder) { unwrapped := (*ErrorResponse)(s) unwrapped.Encode(e) } -// Decode decodes CreateFilterBadRequest from json. -func (s *CreateFilterBadRequest) Decode(d *jx.Decoder) error { +// Decode decodes CliLogoutForbidden from json. +func (s *CliLogoutForbidden) Decode(d *jx.Decoder) error { if s == nil { - return errors.New("invalid: unable to decode CreateFilterBadRequest to nil") + return errors.New("invalid: unable to decode CliLogoutForbidden to nil") } var unwrapped ErrorResponse if err := func() error { @@ -1185,51 +1185,152 @@ func (s *CreateFilterBadRequest) Decode(d *jx.Decoder) error { }(); err != nil { return errors.Wrap(err, "alias") } - *s = CreateFilterBadRequest(unwrapped) + *s = CliLogoutForbidden(unwrapped) return nil } // MarshalJSON implements stdjson.Marshaler. -func (s *CreateFilterBadRequest) MarshalJSON() ([]byte, error) { +func (s *CliLogoutForbidden) MarshalJSON() ([]byte, error) { e := jx.Encoder{} s.Encode(&e) return e.Bytes(), nil } // UnmarshalJSON implements stdjson.Unmarshaler. -func (s *CreateFilterBadRequest) UnmarshalJSON(data []byte) error { +func (s *CliLogoutForbidden) UnmarshalJSON(data []byte) error { d := jx.DecodeBytes(data) return s.Decode(d) } // Encode implements json.Marshaler. -func (s *CreateFilterCreated) Encode(e *jx.Encoder) { +func (s *CliLogoutInput) Encode(e *jx.Encoder) { e.ObjStart() s.encodeFields(e) e.ObjEnd() } // encodeFields encodes fields. -func (s *CreateFilterCreated) encodeFields(e *jx.Encoder) { - { - e.FieldStart("success") - e.Bool(true) - } +func (s *CliLogoutInput) encodeFields(e *jx.Encoder) { { - e.FieldStart("data") - s.Data.Encode(e) + if s.KeyID.Set { + e.FieldStart("key_id") + s.KeyID.Encode(e) + } } } -var jsonFieldsNameOfCreateFilterCreated = [2]string{ - 0: "success", - 1: "data", +var jsonFieldsNameOfCliLogoutInput = [1]string{ + 0: "key_id", } -// Decode decodes CreateFilterCreated from json. -func (s *CreateFilterCreated) Decode(d *jx.Decoder) error { +// Decode decodes CliLogoutInput from json. +func (s *CliLogoutInput) Decode(d *jx.Decoder) error { if s == nil { - return errors.New("invalid: unable to decode CreateFilterCreated to nil") + return errors.New("invalid: unable to decode CliLogoutInput to nil") + } + + if err := d.ObjBytes(func(d *jx.Decoder, k []byte) error { + switch string(k) { + case "key_id": + if err := func() error { + s.KeyID.Reset() + if err := s.KeyID.Decode(d); err != nil { + return err + } + return nil + }(); err != nil { + return errors.Wrap(err, "decode field \"key_id\"") + } + default: + return errors.Errorf("unexpected field %q", k) + } + return nil + }); err != nil { + return errors.Wrap(err, "decode CliLogoutInput") + } + + return nil +} + +// MarshalJSON implements stdjson.Marshaler. +func (s *CliLogoutInput) MarshalJSON() ([]byte, error) { + e := jx.Encoder{} + s.Encode(&e) + return e.Bytes(), nil +} + +// UnmarshalJSON implements stdjson.Unmarshaler. +func (s *CliLogoutInput) UnmarshalJSON(data []byte) error { + d := jx.DecodeBytes(data) + return s.Decode(d) +} + +// Encode encodes CliLogoutNotFound as json. +func (s *CliLogoutNotFound) Encode(e *jx.Encoder) { + unwrapped := (*ErrorResponse)(s) + + unwrapped.Encode(e) +} + +// Decode decodes CliLogoutNotFound from json. +func (s *CliLogoutNotFound) Decode(d *jx.Decoder) error { + if s == nil { + return errors.New("invalid: unable to decode CliLogoutNotFound to nil") + } + var unwrapped ErrorResponse + if err := func() error { + if err := unwrapped.Decode(d); err != nil { + return err + } + return nil + }(); err != nil { + return errors.Wrap(err, "alias") + } + *s = CliLogoutNotFound(unwrapped) + return nil +} + +// MarshalJSON implements stdjson.Marshaler. +func (s *CliLogoutNotFound) MarshalJSON() ([]byte, error) { + e := jx.Encoder{} + s.Encode(&e) + return e.Bytes(), nil +} + +// UnmarshalJSON implements stdjson.Unmarshaler. +func (s *CliLogoutNotFound) UnmarshalJSON(data []byte) error { + d := jx.DecodeBytes(data) + return s.Decode(d) +} + +// Encode implements json.Marshaler. +func (s *CliLogoutOK) Encode(e *jx.Encoder) { + e.ObjStart() + s.encodeFields(e) + e.ObjEnd() +} + +// encodeFields encodes fields. +func (s *CliLogoutOK) encodeFields(e *jx.Encoder) { + { + e.FieldStart("success") + e.Bool(true) + } + { + e.FieldStart("data") + s.Data.Encode(e) + } +} + +var jsonFieldsNameOfCliLogoutOK = [2]string{ + 0: "success", + 1: "data", +} + +// Decode decodes CliLogoutOK from json. +func (s *CliLogoutOK) Decode(d *jx.Decoder) error { + if s == nil { + return errors.New("invalid: unable to decode CliLogoutOK to nil") } var requiredBitSet [1]uint8 @@ -1262,7 +1363,7 @@ func (s *CreateFilterCreated) Decode(d *jx.Decoder) error { } return nil }); err != nil { - return errors.Wrap(err, "decode CreateFilterCreated") + return errors.Wrap(err, "decode CliLogoutOK") } // Validate required fields. var failures []validate.FieldError @@ -1279,8 +1380,8 @@ func (s *CreateFilterCreated) Decode(d *jx.Decoder) error { bitIdx := bits.TrailingZeros8(result) fieldIdx := i*8 + bitIdx var name string - if fieldIdx < len(jsonFieldsNameOfCreateFilterCreated) { - name = jsonFieldsNameOfCreateFilterCreated[fieldIdx] + if fieldIdx < len(jsonFieldsNameOfCliLogoutOK) { + name = jsonFieldsNameOfCliLogoutOK[fieldIdx] } else { name = strconv.Itoa(fieldIdx) } @@ -1301,96 +1402,81 @@ func (s *CreateFilterCreated) Decode(d *jx.Decoder) error { } // MarshalJSON implements stdjson.Marshaler. -func (s *CreateFilterCreated) MarshalJSON() ([]byte, error) { +func (s *CliLogoutOK) MarshalJSON() ([]byte, error) { e := jx.Encoder{} s.Encode(&e) return e.Bytes(), nil } // UnmarshalJSON implements stdjson.Unmarshaler. -func (s *CreateFilterCreated) UnmarshalJSON(data []byte) error { +func (s *CliLogoutOK) UnmarshalJSON(data []byte) error { d := jx.DecodeBytes(data) return s.Decode(d) } // Encode implements json.Marshaler. -func (s *CreateFilterInput) Encode(e *jx.Encoder) { +func (s *CliLogoutResult) Encode(e *jx.Encoder) { e.ObjStart() s.encodeFields(e) e.ObjEnd() } // encodeFields encodes fields. -func (s *CreateFilterInput) encodeFields(e *jx.Encoder) { - { - e.FieldStart("type") - s.Type.Encode(e) - } +func (s *CliLogoutResult) encodeFields(e *jx.Encoder) { { - e.FieldStart("pattern") - e.Str(s.Pattern) + e.FieldStart("revoked") + e.Bool(true) } { - if s.DomainID.Set { - e.FieldStart("domain_id") - s.DomainID.Encode(e) - } + e.FieldStart("key_id") + json.EncodeUUID(e, s.KeyID) } } -var jsonFieldsNameOfCreateFilterInput = [3]string{ - 0: "type", - 1: "pattern", - 2: "domain_id", +var jsonFieldsNameOfCliLogoutResult = [2]string{ + 0: "revoked", + 1: "key_id", } -// Decode decodes CreateFilterInput from json. -func (s *CreateFilterInput) Decode(d *jx.Decoder) error { +// Decode decodes CliLogoutResult from json. +func (s *CliLogoutResult) Decode(d *jx.Decoder) error { if s == nil { - return errors.New("invalid: unable to decode CreateFilterInput to nil") + return errors.New("invalid: unable to decode CliLogoutResult to nil") } var requiredBitSet [1]uint8 if err := d.ObjBytes(func(d *jx.Decoder, k []byte) error { switch string(k) { - case "type": + case "revoked": requiredBitSet[0] |= 1 << 0 if err := func() error { - if err := s.Type.Decode(d); err != nil { + v, err := d.Bool() + s.Revoked = bool(v) + if err != nil { return err } return nil }(); err != nil { - return errors.Wrap(err, "decode field \"type\"") + return errors.Wrap(err, "decode field \"revoked\"") } - case "pattern": + case "key_id": requiredBitSet[0] |= 1 << 1 if err := func() error { - v, err := d.Str() - s.Pattern = string(v) + v, err := json.DecodeUUID(d) + s.KeyID = v if err != nil { return err } return nil }(); err != nil { - return errors.Wrap(err, "decode field \"pattern\"") - } - case "domain_id": - if err := func() error { - s.DomainID.Reset() - if err := s.DomainID.Decode(d); err != nil { - return err - } - return nil - }(); err != nil { - return errors.Wrap(err, "decode field \"domain_id\"") + return errors.Wrap(err, "decode field \"key_id\"") } default: - return errors.Errorf("unexpected field %q", k) + return d.Skip() } return nil }); err != nil { - return errors.Wrap(err, "decode CreateFilterInput") + return errors.Wrap(err, "decode CliLogoutResult") } // Validate required fields. var failures []validate.FieldError @@ -1407,8 +1493,8 @@ func (s *CreateFilterInput) Decode(d *jx.Decoder) error { bitIdx := bits.TrailingZeros8(result) fieldIdx := i*8 + bitIdx var name string - if fieldIdx < len(jsonFieldsNameOfCreateFilterInput) { - name = jsonFieldsNameOfCreateFilterInput[fieldIdx] + if fieldIdx < len(jsonFieldsNameOfCliLogoutResult) { + name = jsonFieldsNameOfCliLogoutResult[fieldIdx] } else { name = strconv.Itoa(fieldIdx) } @@ -1429,69 +1515,29 @@ func (s *CreateFilterInput) Decode(d *jx.Decoder) error { } // MarshalJSON implements stdjson.Marshaler. -func (s *CreateFilterInput) MarshalJSON() ([]byte, error) { - e := jx.Encoder{} - s.Encode(&e) - return e.Bytes(), nil -} - -// UnmarshalJSON implements stdjson.Unmarshaler. -func (s *CreateFilterInput) UnmarshalJSON(data []byte) error { - d := jx.DecodeBytes(data) - return s.Decode(d) -} - -// Encode encodes CreateFilterInputType as json. -func (s CreateFilterInputType) Encode(e *jx.Encoder) { - e.Str(string(s)) -} - -// Decode decodes CreateFilterInputType from json. -func (s *CreateFilterInputType) Decode(d *jx.Decoder) error { - if s == nil { - return errors.New("invalid: unable to decode CreateFilterInputType to nil") - } - v, err := d.StrBytes() - if err != nil { - return err - } - // Try to use constant string. - switch CreateFilterInputType(v) { - case CreateFilterInputTypeWhitelist: - *s = CreateFilterInputTypeWhitelist - case CreateFilterInputTypeBlocklist: - *s = CreateFilterInputTypeBlocklist - default: - *s = CreateFilterInputType(v) - } - - return nil -} - -// MarshalJSON implements stdjson.Marshaler. -func (s CreateFilterInputType) MarshalJSON() ([]byte, error) { +func (s *CliLogoutResult) MarshalJSON() ([]byte, error) { e := jx.Encoder{} s.Encode(&e) return e.Bytes(), nil } // UnmarshalJSON implements stdjson.Unmarshaler. -func (s *CreateFilterInputType) UnmarshalJSON(data []byte) error { +func (s *CliLogoutResult) UnmarshalJSON(data []byte) error { d := jx.DecodeBytes(data) return s.Decode(d) } -// Encode encodes CreateFilterNotFound as json. -func (s *CreateFilterNotFound) Encode(e *jx.Encoder) { +// Encode encodes CliLogoutUnauthorized as json. +func (s *CliLogoutUnauthorized) Encode(e *jx.Encoder) { unwrapped := (*ErrorResponse)(s) unwrapped.Encode(e) } -// Decode decodes CreateFilterNotFound from json. -func (s *CreateFilterNotFound) Decode(d *jx.Decoder) error { +// Decode decodes CliLogoutUnauthorized from json. +func (s *CliLogoutUnauthorized) Decode(d *jx.Decoder) error { if s == nil { - return errors.New("invalid: unable to decode CreateFilterNotFound to nil") + return errors.New("invalid: unable to decode CliLogoutUnauthorized to nil") } var unwrapped ErrorResponse if err := func() error { @@ -1502,34 +1548,34 @@ func (s *CreateFilterNotFound) Decode(d *jx.Decoder) error { }(); err != nil { return errors.Wrap(err, "alias") } - *s = CreateFilterNotFound(unwrapped) + *s = CliLogoutUnauthorized(unwrapped) return nil } // MarshalJSON implements stdjson.Marshaler. -func (s *CreateFilterNotFound) MarshalJSON() ([]byte, error) { +func (s *CliLogoutUnauthorized) MarshalJSON() ([]byte, error) { e := jx.Encoder{} s.Encode(&e) return e.Bytes(), nil } // UnmarshalJSON implements stdjson.Unmarshaler. -func (s *CreateFilterNotFound) UnmarshalJSON(data []byte) error { +func (s *CliLogoutUnauthorized) UnmarshalJSON(data []byte) error { d := jx.DecodeBytes(data) return s.Decode(d) } -// Encode encodes CreateFilterUnauthorized as json. -func (s *CreateFilterUnauthorized) Encode(e *jx.Encoder) { +// Encode encodes CreateEndpointBadRequest as json. +func (s *CreateEndpointBadRequest) Encode(e *jx.Encoder) { unwrapped := (*ErrorResponse)(s) unwrapped.Encode(e) } -// Decode decodes CreateFilterUnauthorized from json. -func (s *CreateFilterUnauthorized) Decode(d *jx.Decoder) error { +// Decode decodes CreateEndpointBadRequest from json. +func (s *CreateEndpointBadRequest) Decode(d *jx.Decoder) error { if s == nil { - return errors.New("invalid: unable to decode CreateFilterUnauthorized to nil") + return errors.New("invalid: unable to decode CreateEndpointBadRequest to nil") } var unwrapped ErrorResponse if err := func() error { @@ -1540,64 +1586,800 @@ func (s *CreateFilterUnauthorized) Decode(d *jx.Decoder) error { }(); err != nil { return errors.Wrap(err, "alias") } - *s = CreateFilterUnauthorized(unwrapped) + *s = CreateEndpointBadRequest(unwrapped) return nil } // MarshalJSON implements stdjson.Marshaler. -func (s *CreateFilterUnauthorized) MarshalJSON() ([]byte, error) { +func (s *CreateEndpointBadRequest) MarshalJSON() ([]byte, error) { e := jx.Encoder{} s.Encode(&e) return e.Bytes(), nil } // UnmarshalJSON implements stdjson.Unmarshaler. -func (s *CreateFilterUnauthorized) UnmarshalJSON(data []byte) error { +func (s *CreateEndpointBadRequest) UnmarshalJSON(data []byte) error { d := jx.DecodeBytes(data) return s.Decode(d) } -// Encode encodes DeleteDomainBadRequest as json. -func (s *DeleteDomainBadRequest) Encode(e *jx.Encoder) { - unwrapped := (*ErrorResponse)(s) - - unwrapped.Encode(e) +// Encode implements json.Marshaler. +func (s *CreateEndpointCreated) Encode(e *jx.Encoder) { + e.ObjStart() + s.encodeFields(e) + e.ObjEnd() } -// Decode decodes DeleteDomainBadRequest from json. -func (s *DeleteDomainBadRequest) Decode(d *jx.Decoder) error { - if s == nil { - return errors.New("invalid: unable to decode DeleteDomainBadRequest to nil") +// encodeFields encodes fields. +func (s *CreateEndpointCreated) encodeFields(e *jx.Encoder) { + { + e.FieldStart("success") + e.Bool(true) } - var unwrapped ErrorResponse - if err := func() error { - if err := unwrapped.Decode(d); err != nil { - return err - } - return nil - }(); err != nil { - return errors.Wrap(err, "alias") + { + e.FieldStart("data") + s.Data.Encode(e) } - *s = DeleteDomainBadRequest(unwrapped) - return nil -} - -// MarshalJSON implements stdjson.Marshaler. -func (s *DeleteDomainBadRequest) MarshalJSON() ([]byte, error) { - e := jx.Encoder{} - s.Encode(&e) - return e.Bytes(), nil } -// UnmarshalJSON implements stdjson.Unmarshaler. -func (s *DeleteDomainBadRequest) UnmarshalJSON(data []byte) error { - d := jx.DecodeBytes(data) - return s.Decode(d) +var jsonFieldsNameOfCreateEndpointCreated = [2]string{ + 0: "success", + 1: "data", } -// Encode encodes DeleteDomainNotFound as json. -func (s *DeleteDomainNotFound) Encode(e *jx.Encoder) { - unwrapped := (*ErrorResponse)(s) +// Decode decodes CreateEndpointCreated from json. +func (s *CreateEndpointCreated) Decode(d *jx.Decoder) error { + if s == nil { + return errors.New("invalid: unable to decode CreateEndpointCreated to nil") + } + var requiredBitSet [1]uint8 + + if err := d.ObjBytes(func(d *jx.Decoder, k []byte) error { + switch string(k) { + case "success": + requiredBitSet[0] |= 1 << 0 + if err := func() error { + v, err := d.Bool() + s.Success = bool(v) + if err != nil { + return err + } + return nil + }(); err != nil { + return errors.Wrap(err, "decode field \"success\"") + } + case "data": + requiredBitSet[0] |= 1 << 1 + if err := func() error { + if err := s.Data.Decode(d); err != nil { + return err + } + return nil + }(); err != nil { + return errors.Wrap(err, "decode field \"data\"") + } + default: + return d.Skip() + } + return nil + }); err != nil { + return errors.Wrap(err, "decode CreateEndpointCreated") + } + // Validate required fields. + var failures []validate.FieldError + for i, mask := range [1]uint8{ + 0b00000011, + } { + if result := (requiredBitSet[i] & mask) ^ mask; result != 0 { + // Mask only required fields and check equality to mask using XOR. + // + // If XOR result is not zero, result is not equal to expected, so some fields are missed. + // Bits of fields which would be set are actually bits of missed fields. + missed := bits.OnesCount8(result) + for bitN := 0; bitN < missed; bitN++ { + bitIdx := bits.TrailingZeros8(result) + fieldIdx := i*8 + bitIdx + var name string + if fieldIdx < len(jsonFieldsNameOfCreateEndpointCreated) { + name = jsonFieldsNameOfCreateEndpointCreated[fieldIdx] + } else { + name = strconv.Itoa(fieldIdx) + } + failures = append(failures, validate.FieldError{ + Name: name, + Error: validate.ErrFieldRequired, + }) + // Reset bit. + result &^= 1 << bitIdx + } + } + } + if len(failures) > 0 { + return &validate.Error{Fields: failures} + } + + return nil +} + +// MarshalJSON implements stdjson.Marshaler. +func (s *CreateEndpointCreated) MarshalJSON() ([]byte, error) { + e := jx.Encoder{} + s.Encode(&e) + return e.Bytes(), nil +} + +// UnmarshalJSON implements stdjson.Unmarshaler. +func (s *CreateEndpointCreated) UnmarshalJSON(data []byte) error { + d := jx.DecodeBytes(data) + return s.Decode(d) +} + +// Encode implements json.Marshaler. +func (s *CreateEndpointInput) Encode(e *jx.Encoder) { + e.ObjStart() + s.encodeFields(e) + e.ObjEnd() +} + +// encodeFields encodes fields. +func (s *CreateEndpointInput) encodeFields(e *jx.Encoder) { + { + e.FieldStart("url") + e.Str(s.URL) + } + { + if s.Enabled.Set { + e.FieldStart("enabled") + s.Enabled.Encode(e) + } + } + { + if s.DomainID.Set { + e.FieldStart("domain_id") + s.DomainID.Encode(e) + } + } + { + if s.Rules != nil { + e.FieldStart("rules") + s.Rules.Encode(e) + } + } +} + +var jsonFieldsNameOfCreateEndpointInput = [4]string{ + 0: "url", + 1: "enabled", + 2: "domain_id", + 3: "rules", +} + +// Decode decodes CreateEndpointInput from json. +func (s *CreateEndpointInput) Decode(d *jx.Decoder) error { + if s == nil { + return errors.New("invalid: unable to decode CreateEndpointInput to nil") + } + var requiredBitSet [1]uint8 + s.setDefaults() + + if err := d.ObjBytes(func(d *jx.Decoder, k []byte) error { + switch string(k) { + case "url": + requiredBitSet[0] |= 1 << 0 + if err := func() error { + v, err := d.Str() + s.URL = string(v) + if err != nil { + return err + } + return nil + }(); err != nil { + return errors.Wrap(err, "decode field \"url\"") + } + case "enabled": + if err := func() error { + s.Enabled.Reset() + if err := s.Enabled.Decode(d); err != nil { + return err + } + return nil + }(); err != nil { + return errors.Wrap(err, "decode field \"enabled\"") + } + case "domain_id": + if err := func() error { + s.DomainID.Reset() + if err := s.DomainID.Decode(d); err != nil { + return err + } + return nil + }(); err != nil { + return errors.Wrap(err, "decode field \"domain_id\"") + } + case "rules": + if err := func() error { + s.Rules = nil + var elem CreateEndpointInputRules + if err := elem.Decode(d); err != nil { + return err + } + s.Rules = &elem + return nil + }(); err != nil { + return errors.Wrap(err, "decode field \"rules\"") + } + default: + return errors.Errorf("unexpected field %q", k) + } + return nil + }); err != nil { + return errors.Wrap(err, "decode CreateEndpointInput") + } + // Validate required fields. + var failures []validate.FieldError + for i, mask := range [1]uint8{ + 0b00000001, + } { + if result := (requiredBitSet[i] & mask) ^ mask; result != 0 { + // Mask only required fields and check equality to mask using XOR. + // + // If XOR result is not zero, result is not equal to expected, so some fields are missed. + // Bits of fields which would be set are actually bits of missed fields. + missed := bits.OnesCount8(result) + for bitN := 0; bitN < missed; bitN++ { + bitIdx := bits.TrailingZeros8(result) + fieldIdx := i*8 + bitIdx + var name string + if fieldIdx < len(jsonFieldsNameOfCreateEndpointInput) { + name = jsonFieldsNameOfCreateEndpointInput[fieldIdx] + } else { + name = strconv.Itoa(fieldIdx) + } + failures = append(failures, validate.FieldError{ + Name: name, + Error: validate.ErrFieldRequired, + }) + // Reset bit. + result &^= 1 << bitIdx + } + } + } + if len(failures) > 0 { + return &validate.Error{Fields: failures} + } + + return nil +} + +// MarshalJSON implements stdjson.Marshaler. +func (s *CreateEndpointInput) MarshalJSON() ([]byte, error) { + e := jx.Encoder{} + s.Encode(&e) + return e.Bytes(), nil +} + +// UnmarshalJSON implements stdjson.Unmarshaler. +func (s *CreateEndpointInput) UnmarshalJSON(data []byte) error { + d := jx.DecodeBytes(data) + return s.Decode(d) +} + +// Encode implements json.Marshaler. +func (s *CreateEndpointInputRules) Encode(e *jx.Encoder) { + e.ObjStart() + s.encodeFields(e) + e.ObjEnd() +} + +// encodeFields encodes fields. +func (s *CreateEndpointInputRules) encodeFields(e *jx.Encoder) { +} + +var jsonFieldsNameOfCreateEndpointInputRules = [0]string{} + +// Decode decodes CreateEndpointInputRules from json. +func (s *CreateEndpointInputRules) Decode(d *jx.Decoder) error { + if s == nil { + return errors.New("invalid: unable to decode CreateEndpointInputRules to nil") + } + + if err := d.ObjBytes(func(d *jx.Decoder, k []byte) error { + switch string(k) { + default: + return d.Skip() + } + }); err != nil { + return errors.Wrap(err, "decode CreateEndpointInputRules") + } + + return nil +} + +// MarshalJSON implements stdjson.Marshaler. +func (s *CreateEndpointInputRules) MarshalJSON() ([]byte, error) { + e := jx.Encoder{} + s.Encode(&e) + return e.Bytes(), nil +} + +// UnmarshalJSON implements stdjson.Unmarshaler. +func (s *CreateEndpointInputRules) UnmarshalJSON(data []byte) error { + d := jx.DecodeBytes(data) + return s.Decode(d) +} + +// Encode encodes CreateEndpointUnauthorized as json. +func (s *CreateEndpointUnauthorized) Encode(e *jx.Encoder) { + unwrapped := (*ErrorResponse)(s) + + unwrapped.Encode(e) +} + +// Decode decodes CreateEndpointUnauthorized from json. +func (s *CreateEndpointUnauthorized) Decode(d *jx.Decoder) error { + if s == nil { + return errors.New("invalid: unable to decode CreateEndpointUnauthorized to nil") + } + var unwrapped ErrorResponse + if err := func() error { + if err := unwrapped.Decode(d); err != nil { + return err + } + return nil + }(); err != nil { + return errors.Wrap(err, "alias") + } + *s = CreateEndpointUnauthorized(unwrapped) + return nil +} + +// MarshalJSON implements stdjson.Marshaler. +func (s *CreateEndpointUnauthorized) MarshalJSON() ([]byte, error) { + e := jx.Encoder{} + s.Encode(&e) + return e.Bytes(), nil +} + +// UnmarshalJSON implements stdjson.Unmarshaler. +func (s *CreateEndpointUnauthorized) UnmarshalJSON(data []byte) error { + d := jx.DecodeBytes(data) + return s.Decode(d) +} + +// Encode encodes CreateFilterBadRequest as json. +func (s *CreateFilterBadRequest) Encode(e *jx.Encoder) { + unwrapped := (*ErrorResponse)(s) + + unwrapped.Encode(e) +} + +// Decode decodes CreateFilterBadRequest from json. +func (s *CreateFilterBadRequest) Decode(d *jx.Decoder) error { + if s == nil { + return errors.New("invalid: unable to decode CreateFilterBadRequest to nil") + } + var unwrapped ErrorResponse + if err := func() error { + if err := unwrapped.Decode(d); err != nil { + return err + } + return nil + }(); err != nil { + return errors.Wrap(err, "alias") + } + *s = CreateFilterBadRequest(unwrapped) + return nil +} + +// MarshalJSON implements stdjson.Marshaler. +func (s *CreateFilterBadRequest) MarshalJSON() ([]byte, error) { + e := jx.Encoder{} + s.Encode(&e) + return e.Bytes(), nil +} + +// UnmarshalJSON implements stdjson.Unmarshaler. +func (s *CreateFilterBadRequest) UnmarshalJSON(data []byte) error { + d := jx.DecodeBytes(data) + return s.Decode(d) +} + +// Encode implements json.Marshaler. +func (s *CreateFilterCreated) Encode(e *jx.Encoder) { + e.ObjStart() + s.encodeFields(e) + e.ObjEnd() +} + +// encodeFields encodes fields. +func (s *CreateFilterCreated) encodeFields(e *jx.Encoder) { + { + e.FieldStart("success") + e.Bool(true) + } + { + e.FieldStart("data") + s.Data.Encode(e) + } +} + +var jsonFieldsNameOfCreateFilterCreated = [2]string{ + 0: "success", + 1: "data", +} + +// Decode decodes CreateFilterCreated from json. +func (s *CreateFilterCreated) Decode(d *jx.Decoder) error { + if s == nil { + return errors.New("invalid: unable to decode CreateFilterCreated to nil") + } + var requiredBitSet [1]uint8 + + if err := d.ObjBytes(func(d *jx.Decoder, k []byte) error { + switch string(k) { + case "success": + requiredBitSet[0] |= 1 << 0 + if err := func() error { + v, err := d.Bool() + s.Success = bool(v) + if err != nil { + return err + } + return nil + }(); err != nil { + return errors.Wrap(err, "decode field \"success\"") + } + case "data": + requiredBitSet[0] |= 1 << 1 + if err := func() error { + if err := s.Data.Decode(d); err != nil { + return err + } + return nil + }(); err != nil { + return errors.Wrap(err, "decode field \"data\"") + } + default: + return d.Skip() + } + return nil + }); err != nil { + return errors.Wrap(err, "decode CreateFilterCreated") + } + // Validate required fields. + var failures []validate.FieldError + for i, mask := range [1]uint8{ + 0b00000011, + } { + if result := (requiredBitSet[i] & mask) ^ mask; result != 0 { + // Mask only required fields and check equality to mask using XOR. + // + // If XOR result is not zero, result is not equal to expected, so some fields are missed. + // Bits of fields which would be set are actually bits of missed fields. + missed := bits.OnesCount8(result) + for bitN := 0; bitN < missed; bitN++ { + bitIdx := bits.TrailingZeros8(result) + fieldIdx := i*8 + bitIdx + var name string + if fieldIdx < len(jsonFieldsNameOfCreateFilterCreated) { + name = jsonFieldsNameOfCreateFilterCreated[fieldIdx] + } else { + name = strconv.Itoa(fieldIdx) + } + failures = append(failures, validate.FieldError{ + Name: name, + Error: validate.ErrFieldRequired, + }) + // Reset bit. + result &^= 1 << bitIdx + } + } + } + if len(failures) > 0 { + return &validate.Error{Fields: failures} + } + + return nil +} + +// MarshalJSON implements stdjson.Marshaler. +func (s *CreateFilterCreated) MarshalJSON() ([]byte, error) { + e := jx.Encoder{} + s.Encode(&e) + return e.Bytes(), nil +} + +// UnmarshalJSON implements stdjson.Unmarshaler. +func (s *CreateFilterCreated) UnmarshalJSON(data []byte) error { + d := jx.DecodeBytes(data) + return s.Decode(d) +} + +// Encode implements json.Marshaler. +func (s *CreateFilterInput) Encode(e *jx.Encoder) { + e.ObjStart() + s.encodeFields(e) + e.ObjEnd() +} + +// encodeFields encodes fields. +func (s *CreateFilterInput) encodeFields(e *jx.Encoder) { + { + e.FieldStart("type") + s.Type.Encode(e) + } + { + e.FieldStart("pattern") + e.Str(s.Pattern) + } + { + if s.DomainID.Set { + e.FieldStart("domain_id") + s.DomainID.Encode(e) + } + } +} + +var jsonFieldsNameOfCreateFilterInput = [3]string{ + 0: "type", + 1: "pattern", + 2: "domain_id", +} + +// Decode decodes CreateFilterInput from json. +func (s *CreateFilterInput) Decode(d *jx.Decoder) error { + if s == nil { + return errors.New("invalid: unable to decode CreateFilterInput to nil") + } + var requiredBitSet [1]uint8 + + if err := d.ObjBytes(func(d *jx.Decoder, k []byte) error { + switch string(k) { + case "type": + requiredBitSet[0] |= 1 << 0 + if err := func() error { + if err := s.Type.Decode(d); err != nil { + return err + } + return nil + }(); err != nil { + return errors.Wrap(err, "decode field \"type\"") + } + case "pattern": + requiredBitSet[0] |= 1 << 1 + if err := func() error { + v, err := d.Str() + s.Pattern = string(v) + if err != nil { + return err + } + return nil + }(); err != nil { + return errors.Wrap(err, "decode field \"pattern\"") + } + case "domain_id": + if err := func() error { + s.DomainID.Reset() + if err := s.DomainID.Decode(d); err != nil { + return err + } + return nil + }(); err != nil { + return errors.Wrap(err, "decode field \"domain_id\"") + } + default: + return errors.Errorf("unexpected field %q", k) + } + return nil + }); err != nil { + return errors.Wrap(err, "decode CreateFilterInput") + } + // Validate required fields. + var failures []validate.FieldError + for i, mask := range [1]uint8{ + 0b00000011, + } { + if result := (requiredBitSet[i] & mask) ^ mask; result != 0 { + // Mask only required fields and check equality to mask using XOR. + // + // If XOR result is not zero, result is not equal to expected, so some fields are missed. + // Bits of fields which would be set are actually bits of missed fields. + missed := bits.OnesCount8(result) + for bitN := 0; bitN < missed; bitN++ { + bitIdx := bits.TrailingZeros8(result) + fieldIdx := i*8 + bitIdx + var name string + if fieldIdx < len(jsonFieldsNameOfCreateFilterInput) { + name = jsonFieldsNameOfCreateFilterInput[fieldIdx] + } else { + name = strconv.Itoa(fieldIdx) + } + failures = append(failures, validate.FieldError{ + Name: name, + Error: validate.ErrFieldRequired, + }) + // Reset bit. + result &^= 1 << bitIdx + } + } + } + if len(failures) > 0 { + return &validate.Error{Fields: failures} + } + + return nil +} + +// MarshalJSON implements stdjson.Marshaler. +func (s *CreateFilterInput) MarshalJSON() ([]byte, error) { + e := jx.Encoder{} + s.Encode(&e) + return e.Bytes(), nil +} + +// UnmarshalJSON implements stdjson.Unmarshaler. +func (s *CreateFilterInput) UnmarshalJSON(data []byte) error { + d := jx.DecodeBytes(data) + return s.Decode(d) +} + +// Encode encodes CreateFilterInputType as json. +func (s CreateFilterInputType) Encode(e *jx.Encoder) { + e.Str(string(s)) +} + +// Decode decodes CreateFilterInputType from json. +func (s *CreateFilterInputType) Decode(d *jx.Decoder) error { + if s == nil { + return errors.New("invalid: unable to decode CreateFilterInputType to nil") + } + v, err := d.StrBytes() + if err != nil { + return err + } + // Try to use constant string. + switch CreateFilterInputType(v) { + case CreateFilterInputTypeWhitelist: + *s = CreateFilterInputTypeWhitelist + case CreateFilterInputTypeBlocklist: + *s = CreateFilterInputTypeBlocklist + default: + *s = CreateFilterInputType(v) + } + + return nil +} + +// MarshalJSON implements stdjson.Marshaler. +func (s CreateFilterInputType) MarshalJSON() ([]byte, error) { + e := jx.Encoder{} + s.Encode(&e) + return e.Bytes(), nil +} + +// UnmarshalJSON implements stdjson.Unmarshaler. +func (s *CreateFilterInputType) UnmarshalJSON(data []byte) error { + d := jx.DecodeBytes(data) + return s.Decode(d) +} + +// Encode encodes CreateFilterNotFound as json. +func (s *CreateFilterNotFound) Encode(e *jx.Encoder) { + unwrapped := (*ErrorResponse)(s) + + unwrapped.Encode(e) +} + +// Decode decodes CreateFilterNotFound from json. +func (s *CreateFilterNotFound) Decode(d *jx.Decoder) error { + if s == nil { + return errors.New("invalid: unable to decode CreateFilterNotFound to nil") + } + var unwrapped ErrorResponse + if err := func() error { + if err := unwrapped.Decode(d); err != nil { + return err + } + return nil + }(); err != nil { + return errors.Wrap(err, "alias") + } + *s = CreateFilterNotFound(unwrapped) + return nil +} + +// MarshalJSON implements stdjson.Marshaler. +func (s *CreateFilterNotFound) MarshalJSON() ([]byte, error) { + e := jx.Encoder{} + s.Encode(&e) + return e.Bytes(), nil +} + +// UnmarshalJSON implements stdjson.Unmarshaler. +func (s *CreateFilterNotFound) UnmarshalJSON(data []byte) error { + d := jx.DecodeBytes(data) + return s.Decode(d) +} + +// Encode encodes CreateFilterUnauthorized as json. +func (s *CreateFilterUnauthorized) Encode(e *jx.Encoder) { + unwrapped := (*ErrorResponse)(s) + + unwrapped.Encode(e) +} + +// Decode decodes CreateFilterUnauthorized from json. +func (s *CreateFilterUnauthorized) Decode(d *jx.Decoder) error { + if s == nil { + return errors.New("invalid: unable to decode CreateFilterUnauthorized to nil") + } + var unwrapped ErrorResponse + if err := func() error { + if err := unwrapped.Decode(d); err != nil { + return err + } + return nil + }(); err != nil { + return errors.Wrap(err, "alias") + } + *s = CreateFilterUnauthorized(unwrapped) + return nil +} + +// MarshalJSON implements stdjson.Marshaler. +func (s *CreateFilterUnauthorized) MarshalJSON() ([]byte, error) { + e := jx.Encoder{} + s.Encode(&e) + return e.Bytes(), nil +} + +// UnmarshalJSON implements stdjson.Unmarshaler. +func (s *CreateFilterUnauthorized) UnmarshalJSON(data []byte) error { + d := jx.DecodeBytes(data) + return s.Decode(d) +} + +// Encode encodes DeleteDomainBadRequest as json. +func (s *DeleteDomainBadRequest) Encode(e *jx.Encoder) { + unwrapped := (*ErrorResponse)(s) + + unwrapped.Encode(e) +} + +// Decode decodes DeleteDomainBadRequest from json. +func (s *DeleteDomainBadRequest) Decode(d *jx.Decoder) error { + if s == nil { + return errors.New("invalid: unable to decode DeleteDomainBadRequest to nil") + } + var unwrapped ErrorResponse + if err := func() error { + if err := unwrapped.Decode(d); err != nil { + return err + } + return nil + }(); err != nil { + return errors.Wrap(err, "alias") + } + *s = DeleteDomainBadRequest(unwrapped) + return nil +} + +// MarshalJSON implements stdjson.Marshaler. +func (s *DeleteDomainBadRequest) MarshalJSON() ([]byte, error) { + e := jx.Encoder{} + s.Encode(&e) + return e.Bytes(), nil +} + +// UnmarshalJSON implements stdjson.Unmarshaler. +func (s *DeleteDomainBadRequest) UnmarshalJSON(data []byte) error { + d := jx.DecodeBytes(data) + return s.Decode(d) +} + +// Encode encodes DeleteDomainNotFound as json. +func (s *DeleteDomainNotFound) Encode(e *jx.Encoder) { + unwrapped := (*ErrorResponse)(s) unwrapped.Encode(e) } @@ -5797,6 +6579,16 @@ func (s *ErrorResponseErrorCode) Decode(d *jx.Decoder) error { *s = ErrorResponseErrorCodeDiscardNotEnabled case ErrorResponseErrorCodeInboundNotRepliable: *s = ErrorResponseErrorCodeInboundNotRepliable + case ErrorResponseErrorCodeAuthorizationPending: + *s = ErrorResponseErrorCodeAuthorizationPending + case ErrorResponseErrorCodeSlowDown: + *s = ErrorResponseErrorCodeSlowDown + case ErrorResponseErrorCodeAccessDenied: + *s = ErrorResponseErrorCodeAccessDenied + case ErrorResponseErrorCodeExpiredToken: + *s = ErrorResponseErrorCodeExpiredToken + case ErrorResponseErrorCodeInvalidDeviceCode: + *s = ErrorResponseErrorCodeInvalidDeviceCode default: *s = ErrorResponseErrorCode(v) } @@ -8674,9 +9466,185 @@ func (s *ListFiltersOK) Decode(d *jx.Decoder) error { case "data": requiredBitSet[0] |= 1 << 1 if err := func() error { - s.Data = make([]Filter, 0) + s.Data = make([]Filter, 0) + if err := d.Arr(func(d *jx.Decoder) error { + var elem Filter + if err := elem.Decode(d); err != nil { + return err + } + s.Data = append(s.Data, elem) + return nil + }); err != nil { + return err + } + return nil + }(); err != nil { + return errors.Wrap(err, "decode field \"data\"") + } + default: + return d.Skip() + } + return nil + }); err != nil { + return errors.Wrap(err, "decode ListFiltersOK") + } + // Validate required fields. + var failures []validate.FieldError + for i, mask := range [1]uint8{ + 0b00000011, + } { + if result := (requiredBitSet[i] & mask) ^ mask; result != 0 { + // Mask only required fields and check equality to mask using XOR. + // + // If XOR result is not zero, result is not equal to expected, so some fields are missed. + // Bits of fields which would be set are actually bits of missed fields. + missed := bits.OnesCount8(result) + for bitN := 0; bitN < missed; bitN++ { + bitIdx := bits.TrailingZeros8(result) + fieldIdx := i*8 + bitIdx + var name string + if fieldIdx < len(jsonFieldsNameOfListFiltersOK) { + name = jsonFieldsNameOfListFiltersOK[fieldIdx] + } else { + name = strconv.Itoa(fieldIdx) + } + failures = append(failures, validate.FieldError{ + Name: name, + Error: validate.ErrFieldRequired, + }) + // Reset bit. + result &^= 1 << bitIdx + } + } + } + if len(failures) > 0 { + return &validate.Error{Fields: failures} + } + + return nil +} + +// MarshalJSON implements stdjson.Marshaler. +func (s *ListFiltersOK) MarshalJSON() ([]byte, error) { + e := jx.Encoder{} + s.Encode(&e) + return e.Bytes(), nil +} + +// UnmarshalJSON implements stdjson.Unmarshaler. +func (s *ListFiltersOK) UnmarshalJSON(data []byte) error { + d := jx.DecodeBytes(data) + return s.Decode(d) +} + +// Encode encodes ListSentEmailsBadRequest as json. +func (s *ListSentEmailsBadRequest) Encode(e *jx.Encoder) { + unwrapped := (*ErrorResponse)(s) + + unwrapped.Encode(e) +} + +// Decode decodes ListSentEmailsBadRequest from json. +func (s *ListSentEmailsBadRequest) Decode(d *jx.Decoder) error { + if s == nil { + return errors.New("invalid: unable to decode ListSentEmailsBadRequest to nil") + } + var unwrapped ErrorResponse + if err := func() error { + if err := unwrapped.Decode(d); err != nil { + return err + } + return nil + }(); err != nil { + return errors.Wrap(err, "alias") + } + *s = ListSentEmailsBadRequest(unwrapped) + return nil +} + +// MarshalJSON implements stdjson.Marshaler. +func (s *ListSentEmailsBadRequest) MarshalJSON() ([]byte, error) { + e := jx.Encoder{} + s.Encode(&e) + return e.Bytes(), nil +} + +// UnmarshalJSON implements stdjson.Unmarshaler. +func (s *ListSentEmailsBadRequest) UnmarshalJSON(data []byte) error { + d := jx.DecodeBytes(data) + return s.Decode(d) +} + +// Encode implements json.Marshaler. +func (s *ListSentEmailsOK) Encode(e *jx.Encoder) { + e.ObjStart() + s.encodeFields(e) + e.ObjEnd() +} + +// encodeFields encodes fields. +func (s *ListSentEmailsOK) encodeFields(e *jx.Encoder) { + { + e.FieldStart("success") + e.Bool(true) + } + { + e.FieldStart("meta") + s.Meta.Encode(e) + } + { + e.FieldStart("data") + e.ArrStart() + for _, elem := range s.Data { + elem.Encode(e) + } + e.ArrEnd() + } +} + +var jsonFieldsNameOfListSentEmailsOK = [3]string{ + 0: "success", + 1: "meta", + 2: "data", +} + +// Decode decodes ListSentEmailsOK from json. +func (s *ListSentEmailsOK) Decode(d *jx.Decoder) error { + if s == nil { + return errors.New("invalid: unable to decode ListSentEmailsOK to nil") + } + var requiredBitSet [1]uint8 + + if err := d.ObjBytes(func(d *jx.Decoder, k []byte) error { + switch string(k) { + case "success": + requiredBitSet[0] |= 1 << 0 + if err := func() error { + v, err := d.Bool() + s.Success = bool(v) + if err != nil { + return err + } + return nil + }(); err != nil { + return errors.Wrap(err, "decode field \"success\"") + } + case "meta": + requiredBitSet[0] |= 1 << 1 + if err := func() error { + if err := s.Meta.Decode(d); err != nil { + return err + } + return nil + }(); err != nil { + return errors.Wrap(err, "decode field \"meta\"") + } + case "data": + requiredBitSet[0] |= 1 << 2 + if err := func() error { + s.Data = make([]SentEmailSummary, 0) if err := d.Arr(func(d *jx.Decoder) error { - var elem Filter + var elem SentEmailSummary if err := elem.Decode(d); err != nil { return err } @@ -8694,12 +9662,12 @@ func (s *ListFiltersOK) Decode(d *jx.Decoder) error { } return nil }); err != nil { - return errors.Wrap(err, "decode ListFiltersOK") + return errors.Wrap(err, "decode ListSentEmailsOK") } // Validate required fields. var failures []validate.FieldError for i, mask := range [1]uint8{ - 0b00000011, + 0b00000111, } { if result := (requiredBitSet[i] & mask) ^ mask; result != 0 { // Mask only required fields and check equality to mask using XOR. @@ -8711,8 +9679,8 @@ func (s *ListFiltersOK) Decode(d *jx.Decoder) error { bitIdx := bits.TrailingZeros8(result) fieldIdx := i*8 + bitIdx var name string - if fieldIdx < len(jsonFieldsNameOfListFiltersOK) { - name = jsonFieldsNameOfListFiltersOK[fieldIdx] + if fieldIdx < len(jsonFieldsNameOfListSentEmailsOK) { + name = jsonFieldsNameOfListSentEmailsOK[fieldIdx] } else { name = strconv.Itoa(fieldIdx) } @@ -8733,327 +9701,518 @@ func (s *ListFiltersOK) Decode(d *jx.Decoder) error { } // MarshalJSON implements stdjson.Marshaler. -func (s *ListFiltersOK) MarshalJSON() ([]byte, error) { +func (s *ListSentEmailsOK) MarshalJSON() ([]byte, error) { + e := jx.Encoder{} + s.Encode(&e) + return e.Bytes(), nil +} + +// UnmarshalJSON implements stdjson.Unmarshaler. +func (s *ListSentEmailsOK) UnmarshalJSON(data []byte) error { + d := jx.DecodeBytes(data) + return s.Decode(d) +} + +// Encode encodes ListSentEmailsUnauthorized as json. +func (s *ListSentEmailsUnauthorized) Encode(e *jx.Encoder) { + unwrapped := (*ErrorResponse)(s) + + unwrapped.Encode(e) +} + +// Decode decodes ListSentEmailsUnauthorized from json. +func (s *ListSentEmailsUnauthorized) Decode(d *jx.Decoder) error { + if s == nil { + return errors.New("invalid: unable to decode ListSentEmailsUnauthorized to nil") + } + var unwrapped ErrorResponse + if err := func() error { + if err := unwrapped.Decode(d); err != nil { + return err + } + return nil + }(); err != nil { + return errors.Wrap(err, "alias") + } + *s = ListSentEmailsUnauthorized(unwrapped) + return nil +} + +// MarshalJSON implements stdjson.Marshaler. +func (s *ListSentEmailsUnauthorized) MarshalJSON() ([]byte, error) { + e := jx.Encoder{} + s.Encode(&e) + return e.Bytes(), nil +} + +// UnmarshalJSON implements stdjson.Unmarshaler. +func (s *ListSentEmailsUnauthorized) UnmarshalJSON(data []byte) error { + d := jx.DecodeBytes(data) + return s.Decode(d) +} + +// Encode encodes string as json. +func (o NilString) Encode(e *jx.Encoder) { + if o.Null { + e.Null() + return + } + e.Str(string(o.Value)) +} + +// Decode decodes string from json. +func (o *NilString) Decode(d *jx.Decoder) error { + if o == nil { + return errors.New("invalid: unable to decode NilString to nil") + } + if d.Next() == jx.Null { + if err := d.Null(); err != nil { + return err + } + + var v string + o.Value = v + o.Null = true + return nil + } + o.Null = false + v, err := d.Str() + if err != nil { + return err + } + o.Value = string(v) + return nil +} + +// MarshalJSON implements stdjson.Marshaler. +func (s NilString) MarshalJSON() ([]byte, error) { + e := jx.Encoder{} + s.Encode(&e) + return e.Bytes(), nil +} + +// UnmarshalJSON implements stdjson.Unmarshaler. +func (s *NilString) UnmarshalJSON(data []byte) error { + d := jx.DecodeBytes(data) + return s.Decode(d) +} + +// Encode encodes bool as json. +func (o OptBool) Encode(e *jx.Encoder) { + if !o.Set { + return + } + e.Bool(bool(o.Value)) +} + +// Decode decodes bool from json. +func (o *OptBool) Decode(d *jx.Decoder) error { + if o == nil { + return errors.New("invalid: unable to decode OptBool to nil") + } + o.Set = true + v, err := d.Bool() + if err != nil { + return err + } + o.Value = bool(v) + return nil +} + +// MarshalJSON implements stdjson.Marshaler. +func (s OptBool) MarshalJSON() ([]byte, error) { + e := jx.Encoder{} + s.Encode(&e) + return e.Bytes(), nil +} + +// UnmarshalJSON implements stdjson.Unmarshaler. +func (s *OptBool) UnmarshalJSON(data []byte) error { + d := jx.DecodeBytes(data) + return s.Decode(d) +} + +// Encode encodes CliLogoutInput as json. +func (o OptCliLogoutInput) Encode(e *jx.Encoder) { + if !o.Set { + return + } + o.Value.Encode(e) +} + +// Decode decodes CliLogoutInput from json. +func (o *OptCliLogoutInput) Decode(d *jx.Decoder) error { + if o == nil { + return errors.New("invalid: unable to decode OptCliLogoutInput to nil") + } + o.Set = true + if err := o.Value.Decode(d); err != nil { + return err + } + return nil +} + +// MarshalJSON implements stdjson.Marshaler. +func (s OptCliLogoutInput) MarshalJSON() ([]byte, error) { + e := jx.Encoder{} + s.Encode(&e) + return e.Bytes(), nil +} + +// UnmarshalJSON implements stdjson.Unmarshaler. +func (s *OptCliLogoutInput) UnmarshalJSON(data []byte) error { + d := jx.DecodeBytes(data) + return s.Decode(d) +} + +// Encode encodes DeliveryStatus as json. +func (o OptDeliveryStatus) Encode(e *jx.Encoder) { + if !o.Set { + return + } + e.Str(string(o.Value)) +} + +// Decode decodes DeliveryStatus from json. +func (o *OptDeliveryStatus) Decode(d *jx.Decoder) error { + if o == nil { + return errors.New("invalid: unable to decode OptDeliveryStatus to nil") + } + o.Set = true + if err := o.Value.Decode(d); err != nil { + return err + } + return nil +} + +// MarshalJSON implements stdjson.Marshaler. +func (s OptDeliveryStatus) MarshalJSON() ([]byte, error) { + e := jx.Encoder{} + s.Encode(&e) + return e.Bytes(), nil +} + +// UnmarshalJSON implements stdjson.Unmarshaler. +func (s *OptDeliveryStatus) UnmarshalJSON(data []byte) error { + d := jx.DecodeBytes(data) + return s.Decode(d) +} + +// Encode encodes ErrorResponseErrorDetails as json. +func (o OptErrorResponseErrorDetails) Encode(e *jx.Encoder) { + if !o.Set { + return + } + o.Value.Encode(e) +} + +// Decode decodes ErrorResponseErrorDetails from json. +func (o *OptErrorResponseErrorDetails) Decode(d *jx.Decoder) error { + if o == nil { + return errors.New("invalid: unable to decode OptErrorResponseErrorDetails to nil") + } + o.Set = true + if err := o.Value.Decode(d); err != nil { + return err + } + return nil +} + +// MarshalJSON implements stdjson.Marshaler. +func (s OptErrorResponseErrorDetails) MarshalJSON() ([]byte, error) { + e := jx.Encoder{} + s.Encode(&e) + return e.Bytes(), nil +} + +// UnmarshalJSON implements stdjson.Unmarshaler. +func (s *OptErrorResponseErrorDetails) UnmarshalJSON(data []byte) error { + d := jx.DecodeBytes(data) + return s.Decode(d) +} + +// Encode encodes ErrorResponseErrorDetailsMxConflict as json. +func (o OptErrorResponseErrorDetailsMxConflict) Encode(e *jx.Encoder) { + if !o.Set { + return + } + o.Value.Encode(e) +} + +// Decode decodes ErrorResponseErrorDetailsMxConflict from json. +func (o *OptErrorResponseErrorDetailsMxConflict) Decode(d *jx.Decoder) error { + if o == nil { + return errors.New("invalid: unable to decode OptErrorResponseErrorDetailsMxConflict to nil") + } + o.Set = true + if err := o.Value.Decode(d); err != nil { + return err + } + return nil +} + +// MarshalJSON implements stdjson.Marshaler. +func (s OptErrorResponseErrorDetailsMxConflict) MarshalJSON() ([]byte, error) { e := jx.Encoder{} s.Encode(&e) return e.Bytes(), nil } // UnmarshalJSON implements stdjson.Unmarshaler. -func (s *ListFiltersOK) UnmarshalJSON(data []byte) error { +func (s *OptErrorResponseErrorDetailsMxConflict) UnmarshalJSON(data []byte) error { d := jx.DecodeBytes(data) return s.Decode(d) } -// Encode encodes ListSentEmailsBadRequest as json. -func (s *ListSentEmailsBadRequest) Encode(e *jx.Encoder) { - unwrapped := (*ErrorResponse)(s) - - unwrapped.Encode(e) +// Encode encodes GateFix as json. +func (o OptGateFix) Encode(e *jx.Encoder) { + if !o.Set { + return + } + o.Value.Encode(e) } -// Decode decodes ListSentEmailsBadRequest from json. -func (s *ListSentEmailsBadRequest) Decode(d *jx.Decoder) error { - if s == nil { - return errors.New("invalid: unable to decode ListSentEmailsBadRequest to nil") +// Decode decodes GateFix from json. +func (o *OptGateFix) Decode(d *jx.Decoder) error { + if o == nil { + return errors.New("invalid: unable to decode OptGateFix to nil") } - var unwrapped ErrorResponse - if err := func() error { - if err := unwrapped.Decode(d); err != nil { - return err - } - return nil - }(); err != nil { - return errors.Wrap(err, "alias") + o.Set = true + if err := o.Value.Decode(d); err != nil { + return err } - *s = ListSentEmailsBadRequest(unwrapped) return nil } // MarshalJSON implements stdjson.Marshaler. -func (s *ListSentEmailsBadRequest) MarshalJSON() ([]byte, error) { +func (s OptGateFix) MarshalJSON() ([]byte, error) { e := jx.Encoder{} s.Encode(&e) return e.Bytes(), nil } // UnmarshalJSON implements stdjson.Unmarshaler. -func (s *ListSentEmailsBadRequest) UnmarshalJSON(data []byte) error { +func (s *OptGateFix) UnmarshalJSON(data []byte) error { d := jx.DecodeBytes(data) return s.Decode(d) } -// Encode implements json.Marshaler. -func (s *ListSentEmailsOK) Encode(e *jx.Encoder) { - e.ObjStart() - s.encodeFields(e) - e.ObjEnd() -} - -// encodeFields encodes fields. -func (s *ListSentEmailsOK) encodeFields(e *jx.Encoder) { - { - e.FieldStart("success") - e.Bool(true) - } - { - e.FieldStart("meta") - s.Meta.Encode(e) - } - { - e.FieldStart("data") - e.ArrStart() - for _, elem := range s.Data { - elem.Encode(e) - } - e.ArrEnd() +// Encode encodes int as json. +func (o OptInt) Encode(e *jx.Encoder) { + if !o.Set { + return } + e.Int(int(o.Value)) } -var jsonFieldsNameOfListSentEmailsOK = [3]string{ - 0: "success", - 1: "meta", - 2: "data", -} - -// Decode decodes ListSentEmailsOK from json. -func (s *ListSentEmailsOK) Decode(d *jx.Decoder) error { - if s == nil { - return errors.New("invalid: unable to decode ListSentEmailsOK to nil") - } - var requiredBitSet [1]uint8 - - if err := d.ObjBytes(func(d *jx.Decoder, k []byte) error { - switch string(k) { - case "success": - requiredBitSet[0] |= 1 << 0 - if err := func() error { - v, err := d.Bool() - s.Success = bool(v) - if err != nil { - return err - } - return nil - }(); err != nil { - return errors.Wrap(err, "decode field \"success\"") - } - case "meta": - requiredBitSet[0] |= 1 << 1 - if err := func() error { - if err := s.Meta.Decode(d); err != nil { - return err - } - return nil - }(); err != nil { - return errors.Wrap(err, "decode field \"meta\"") - } - case "data": - requiredBitSet[0] |= 1 << 2 - if err := func() error { - s.Data = make([]SentEmailSummary, 0) - if err := d.Arr(func(d *jx.Decoder) error { - var elem SentEmailSummary - if err := elem.Decode(d); err != nil { - return err - } - s.Data = append(s.Data, elem) - return nil - }); err != nil { - return err - } - return nil - }(); err != nil { - return errors.Wrap(err, "decode field \"data\"") - } - default: - return d.Skip() - } - return nil - }); err != nil { - return errors.Wrap(err, "decode ListSentEmailsOK") - } - // Validate required fields. - var failures []validate.FieldError - for i, mask := range [1]uint8{ - 0b00000111, - } { - if result := (requiredBitSet[i] & mask) ^ mask; result != 0 { - // Mask only required fields and check equality to mask using XOR. - // - // If XOR result is not zero, result is not equal to expected, so some fields are missed. - // Bits of fields which would be set are actually bits of missed fields. - missed := bits.OnesCount8(result) - for bitN := 0; bitN < missed; bitN++ { - bitIdx := bits.TrailingZeros8(result) - fieldIdx := i*8 + bitIdx - var name string - if fieldIdx < len(jsonFieldsNameOfListSentEmailsOK) { - name = jsonFieldsNameOfListSentEmailsOK[fieldIdx] - } else { - name = strconv.Itoa(fieldIdx) - } - failures = append(failures, validate.FieldError{ - Name: name, - Error: validate.ErrFieldRequired, - }) - // Reset bit. - result &^= 1 << bitIdx - } - } +// Decode decodes int from json. +func (o *OptInt) Decode(d *jx.Decoder) error { + if o == nil { + return errors.New("invalid: unable to decode OptInt to nil") } - if len(failures) > 0 { - return &validate.Error{Fields: failures} + o.Set = true + v, err := d.Int() + if err != nil { + return err } - + o.Value = int(v) return nil } // MarshalJSON implements stdjson.Marshaler. -func (s *ListSentEmailsOK) MarshalJSON() ([]byte, error) { +func (s OptInt) MarshalJSON() ([]byte, error) { e := jx.Encoder{} s.Encode(&e) return e.Bytes(), nil } // UnmarshalJSON implements stdjson.Unmarshaler. -func (s *ListSentEmailsOK) UnmarshalJSON(data []byte) error { +func (s *OptInt) UnmarshalJSON(data []byte) error { d := jx.DecodeBytes(data) return s.Decode(d) } -// Encode encodes ListSentEmailsUnauthorized as json. -func (s *ListSentEmailsUnauthorized) Encode(e *jx.Encoder) { - unwrapped := (*ErrorResponse)(s) - - unwrapped.Encode(e) +// Encode encodes bool as json. +func (o OptNilBool) Encode(e *jx.Encoder) { + if !o.Set { + return + } + if o.Null { + e.Null() + return + } + e.Bool(bool(o.Value)) } -// Decode decodes ListSentEmailsUnauthorized from json. -func (s *ListSentEmailsUnauthorized) Decode(d *jx.Decoder) error { - if s == nil { - return errors.New("invalid: unable to decode ListSentEmailsUnauthorized to nil") +// Decode decodes bool from json. +func (o *OptNilBool) Decode(d *jx.Decoder) error { + if o == nil { + return errors.New("invalid: unable to decode OptNilBool to nil") } - var unwrapped ErrorResponse - if err := func() error { - if err := unwrapped.Decode(d); err != nil { + if d.Next() == jx.Null { + if err := d.Null(); err != nil { return err } + + var v bool + o.Value = v + o.Set = true + o.Null = true return nil - }(); err != nil { - return errors.Wrap(err, "alias") } - *s = ListSentEmailsUnauthorized(unwrapped) + o.Set = true + o.Null = false + v, err := d.Bool() + if err != nil { + return err + } + o.Value = bool(v) return nil } // MarshalJSON implements stdjson.Marshaler. -func (s *ListSentEmailsUnauthorized) MarshalJSON() ([]byte, error) { +func (s OptNilBool) MarshalJSON() ([]byte, error) { e := jx.Encoder{} s.Encode(&e) return e.Bytes(), nil } // UnmarshalJSON implements stdjson.Unmarshaler. -func (s *ListSentEmailsUnauthorized) UnmarshalJSON(data []byte) error { +func (s *OptNilBool) UnmarshalJSON(data []byte) error { d := jx.DecodeBytes(data) return s.Decode(d) } -// Encode encodes string as json. -func (o NilString) Encode(e *jx.Encoder) { +// Encode encodes time.Time as json. +func (o OptNilDateTime) Encode(e *jx.Encoder, format func(*jx.Encoder, time.Time)) { + if !o.Set { + return + } if o.Null { e.Null() return } - e.Str(string(o.Value)) + format(e, o.Value) } -// Decode decodes string from json. -func (o *NilString) Decode(d *jx.Decoder) error { +// Decode decodes time.Time from json. +func (o *OptNilDateTime) Decode(d *jx.Decoder, format func(*jx.Decoder) (time.Time, error)) error { if o == nil { - return errors.New("invalid: unable to decode NilString to nil") + return errors.New("invalid: unable to decode OptNilDateTime to nil") } if d.Next() == jx.Null { if err := d.Null(); err != nil { return err } - var v string + var v time.Time o.Value = v + o.Set = true o.Null = true return nil } + o.Set = true o.Null = false - v, err := d.Str() + v, err := format(d) if err != nil { return err } - o.Value = string(v) + o.Value = v return nil } // MarshalJSON implements stdjson.Marshaler. -func (s NilString) MarshalJSON() ([]byte, error) { +func (s OptNilDateTime) MarshalJSON() ([]byte, error) { e := jx.Encoder{} - s.Encode(&e) + s.Encode(&e, json.EncodeDateTime) return e.Bytes(), nil } // UnmarshalJSON implements stdjson.Unmarshaler. -func (s *NilString) UnmarshalJSON(data []byte) error { +func (s *OptNilDateTime) UnmarshalJSON(data []byte) error { d := jx.DecodeBytes(data) - return s.Decode(d) + return s.Decode(d, json.DecodeDateTime) } -// Encode encodes bool as json. -func (o OptBool) Encode(e *jx.Encoder) { +// Encode encodes DeliverySummaryEmail as json. +func (o OptNilDeliverySummaryEmail) Encode(e *jx.Encoder) { if !o.Set { return } - e.Bool(bool(o.Value)) + if o.Null { + e.Null() + return + } + o.Value.Encode(e) } -// Decode decodes bool from json. -func (o *OptBool) Decode(d *jx.Decoder) error { +// Decode decodes DeliverySummaryEmail from json. +func (o *OptNilDeliverySummaryEmail) Decode(d *jx.Decoder) error { if o == nil { - return errors.New("invalid: unable to decode OptBool to nil") + return errors.New("invalid: unable to decode OptNilDeliverySummaryEmail to nil") + } + if d.Next() == jx.Null { + if err := d.Null(); err != nil { + return err + } + + var v DeliverySummaryEmail + o.Value = v + o.Set = true + o.Null = true + return nil } o.Set = true - v, err := d.Bool() - if err != nil { + o.Null = false + if err := o.Value.Decode(d); err != nil { return err } - o.Value = bool(v) return nil } // MarshalJSON implements stdjson.Marshaler. -func (s OptBool) MarshalJSON() ([]byte, error) { +func (s OptNilDeliverySummaryEmail) MarshalJSON() ([]byte, error) { e := jx.Encoder{} s.Encode(&e) return e.Bytes(), nil } // UnmarshalJSON implements stdjson.Unmarshaler. -func (s *OptBool) UnmarshalJSON(data []byte) error { +func (s *OptNilDeliverySummaryEmail) UnmarshalJSON(data []byte) error { d := jx.DecodeBytes(data) return s.Decode(d) } -// Encode encodes DeliveryStatus as json. -func (o OptDeliveryStatus) Encode(e *jx.Encoder) { +// Encode encodes EmailWebhookStatus as json. +func (o OptNilEmailWebhookStatus) Encode(e *jx.Encoder) { if !o.Set { return } + if o.Null { + e.Null() + return + } e.Str(string(o.Value)) } -// Decode decodes DeliveryStatus from json. -func (o *OptDeliveryStatus) Decode(d *jx.Decoder) error { +// Decode decodes EmailWebhookStatus from json. +func (o *OptNilEmailWebhookStatus) Decode(d *jx.Decoder) error { if o == nil { - return errors.New("invalid: unable to decode OptDeliveryStatus to nil") + return errors.New("invalid: unable to decode OptNilEmailWebhookStatus to nil") + } + if d.Next() == jx.Null { + if err := d.Null(); err != nil { + return err + } + + var v EmailWebhookStatus + o.Value = v + o.Set = true + o.Null = true + return nil } o.Set = true + o.Null = false if err := o.Value.Decode(d); err != nil { return err } @@ -9061,154 +10220,234 @@ func (o *OptDeliveryStatus) Decode(d *jx.Decoder) error { } // MarshalJSON implements stdjson.Marshaler. -func (s OptDeliveryStatus) MarshalJSON() ([]byte, error) { +func (s OptNilEmailWebhookStatus) MarshalJSON() ([]byte, error) { e := jx.Encoder{} s.Encode(&e) return e.Bytes(), nil } // UnmarshalJSON implements stdjson.Unmarshaler. -func (s *OptDeliveryStatus) UnmarshalJSON(data []byte) error { +func (s *OptNilEmailWebhookStatus) UnmarshalJSON(data []byte) error { d := jx.DecodeBytes(data) return s.Decode(d) } -// Encode encodes ErrorResponseErrorDetails as json. -func (o OptErrorResponseErrorDetails) Encode(e *jx.Encoder) { +// Encode encodes float64 as json. +func (o OptNilFloat64) Encode(e *jx.Encoder) { if !o.Set { return } - o.Value.Encode(e) + if o.Null { + e.Null() + return + } + e.Float64(float64(o.Value)) } -// Decode decodes ErrorResponseErrorDetails from json. -func (o *OptErrorResponseErrorDetails) Decode(d *jx.Decoder) error { +// Decode decodes float64 from json. +func (o *OptNilFloat64) Decode(d *jx.Decoder) error { if o == nil { - return errors.New("invalid: unable to decode OptErrorResponseErrorDetails to nil") + return errors.New("invalid: unable to decode OptNilFloat64 to nil") + } + if d.Next() == jx.Null { + if err := d.Null(); err != nil { + return err + } + + var v float64 + o.Value = v + o.Set = true + o.Null = true + return nil } o.Set = true - if err := o.Value.Decode(d); err != nil { + o.Null = false + v, err := d.Float64() + if err != nil { return err } + o.Value = float64(v) return nil } // MarshalJSON implements stdjson.Marshaler. -func (s OptErrorResponseErrorDetails) MarshalJSON() ([]byte, error) { +func (s OptNilFloat64) MarshalJSON() ([]byte, error) { e := jx.Encoder{} s.Encode(&e) return e.Bytes(), nil } // UnmarshalJSON implements stdjson.Unmarshaler. -func (s *OptErrorResponseErrorDetails) UnmarshalJSON(data []byte) error { +func (s *OptNilFloat64) UnmarshalJSON(data []byte) error { d := jx.DecodeBytes(data) return s.Decode(d) } -// Encode encodes ErrorResponseErrorDetailsMxConflict as json. -func (o OptErrorResponseErrorDetailsMxConflict) Encode(e *jx.Encoder) { +// Encode encodes []GateDenial as json. +func (o OptNilGateDenialArray) Encode(e *jx.Encoder) { if !o.Set { return } - o.Value.Encode(e) + if o.Null { + e.Null() + return + } + e.ArrStart() + for _, elem := range o.Value { + elem.Encode(e) + } + e.ArrEnd() } -// Decode decodes ErrorResponseErrorDetailsMxConflict from json. -func (o *OptErrorResponseErrorDetailsMxConflict) Decode(d *jx.Decoder) error { +// Decode decodes []GateDenial from json. +func (o *OptNilGateDenialArray) Decode(d *jx.Decoder) error { if o == nil { - return errors.New("invalid: unable to decode OptErrorResponseErrorDetailsMxConflict to nil") + return errors.New("invalid: unable to decode OptNilGateDenialArray to nil") + } + if d.Next() == jx.Null { + if err := d.Null(); err != nil { + return err + } + + var v []GateDenial + o.Value = v + o.Set = true + o.Null = true + return nil } o.Set = true - if err := o.Value.Decode(d); err != nil { + o.Null = false + o.Value = make([]GateDenial, 0) + if err := d.Arr(func(d *jx.Decoder) error { + var elem GateDenial + if err := elem.Decode(d); err != nil { + return err + } + o.Value = append(o.Value, elem) + return nil + }); err != nil { return err } return nil } // MarshalJSON implements stdjson.Marshaler. -func (s OptErrorResponseErrorDetailsMxConflict) MarshalJSON() ([]byte, error) { +func (s OptNilGateDenialArray) MarshalJSON() ([]byte, error) { e := jx.Encoder{} s.Encode(&e) return e.Bytes(), nil } // UnmarshalJSON implements stdjson.Unmarshaler. -func (s *OptErrorResponseErrorDetailsMxConflict) UnmarshalJSON(data []byte) error { +func (s *OptNilGateDenialArray) UnmarshalJSON(data []byte) error { d := jx.DecodeBytes(data) return s.Decode(d) } -// Encode encodes GateFix as json. -func (o OptGateFix) Encode(e *jx.Encoder) { +// Encode encodes int as json. +func (o OptNilInt) Encode(e *jx.Encoder) { if !o.Set { return } - o.Value.Encode(e) + if o.Null { + e.Null() + return + } + e.Int(int(o.Value)) } -// Decode decodes GateFix from json. -func (o *OptGateFix) Decode(d *jx.Decoder) error { +// Decode decodes int from json. +func (o *OptNilInt) Decode(d *jx.Decoder) error { if o == nil { - return errors.New("invalid: unable to decode OptGateFix to nil") + return errors.New("invalid: unable to decode OptNilInt to nil") + } + if d.Next() == jx.Null { + if err := d.Null(); err != nil { + return err + } + + var v int + o.Value = v + o.Set = true + o.Null = true + return nil } o.Set = true - if err := o.Value.Decode(d); err != nil { + o.Null = false + v, err := d.Int() + if err != nil { return err } + o.Value = int(v) return nil } // MarshalJSON implements stdjson.Marshaler. -func (s OptGateFix) MarshalJSON() ([]byte, error) { +func (s OptNilInt) MarshalJSON() ([]byte, error) { e := jx.Encoder{} s.Encode(&e) return e.Bytes(), nil } // UnmarshalJSON implements stdjson.Unmarshaler. -func (s *OptGateFix) UnmarshalJSON(data []byte) error { +func (s *OptNilInt) UnmarshalJSON(data []byte) error { d := jx.DecodeBytes(data) return s.Decode(d) } -// Encode encodes int as json. -func (o OptInt) Encode(e *jx.Encoder) { +// Encode encodes string as json. +func (o OptNilString) Encode(e *jx.Encoder) { if !o.Set { return } - e.Int(int(o.Value)) + if o.Null { + e.Null() + return + } + e.Str(string(o.Value)) } -// Decode decodes int from json. -func (o *OptInt) Decode(d *jx.Decoder) error { +// Decode decodes string from json. +func (o *OptNilString) Decode(d *jx.Decoder) error { if o == nil { - return errors.New("invalid: unable to decode OptInt to nil") + return errors.New("invalid: unable to decode OptNilString to nil") + } + if d.Next() == jx.Null { + if err := d.Null(); err != nil { + return err + } + + var v string + o.Value = v + o.Set = true + o.Null = true + return nil } o.Set = true - v, err := d.Int() + o.Null = false + v, err := d.Str() if err != nil { return err } - o.Value = int(v) + o.Value = string(v) return nil } // MarshalJSON implements stdjson.Marshaler. -func (s OptInt) MarshalJSON() ([]byte, error) { +func (s OptNilString) MarshalJSON() ([]byte, error) { e := jx.Encoder{} s.Encode(&e) return e.Bytes(), nil } // UnmarshalJSON implements stdjson.Unmarshaler. -func (s *OptInt) UnmarshalJSON(data []byte) error { +func (s *OptNilString) UnmarshalJSON(data []byte) error { d := jx.DecodeBytes(data) return s.Decode(d) } -// Encode encodes bool as json. -func (o OptNilBool) Encode(e *jx.Encoder) { +// Encode encodes []string as json. +func (o OptNilStringArray) Encode(e *jx.Encoder) { if !o.Set { return } @@ -9216,20 +10455,24 @@ func (o OptNilBool) Encode(e *jx.Encoder) { e.Null() return } - e.Bool(bool(o.Value)) + e.ArrStart() + for _, elem := range o.Value { + e.Str(elem) + } + e.ArrEnd() } -// Decode decodes bool from json. -func (o *OptNilBool) Decode(d *jx.Decoder) error { +// Decode decodes []string from json. +func (o *OptNilStringArray) Decode(d *jx.Decoder) error { if o == nil { - return errors.New("invalid: unable to decode OptNilBool to nil") + return errors.New("invalid: unable to decode OptNilStringArray to nil") } if d.Next() == jx.Null { if err := d.Null(); err != nil { return err } - var v bool + var v []string o.Value = v o.Set = true o.Null = true @@ -9237,29 +10480,37 @@ func (o *OptNilBool) Decode(d *jx.Decoder) error { } o.Set = true o.Null = false - v, err := d.Bool() - if err != nil { + o.Value = make([]string, 0) + if err := d.Arr(func(d *jx.Decoder) error { + var elem string + v, err := d.Str() + elem = string(v) + if err != nil { + return err + } + o.Value = append(o.Value, elem) + return nil + }); err != nil { return err } - o.Value = bool(v) return nil } // MarshalJSON implements stdjson.Marshaler. -func (s OptNilBool) MarshalJSON() ([]byte, error) { +func (s OptNilStringArray) MarshalJSON() ([]byte, error) { e := jx.Encoder{} s.Encode(&e) return e.Bytes(), nil } // UnmarshalJSON implements stdjson.Unmarshaler. -func (s *OptNilBool) UnmarshalJSON(data []byte) error { +func (s *OptNilStringArray) UnmarshalJSON(data []byte) error { d := jx.DecodeBytes(data) return s.Decode(d) } -// Encode encodes time.Time as json. -func (o OptNilDateTime) Encode(e *jx.Encoder, format func(*jx.Encoder, time.Time)) { +// Encode encodes uuid.UUID as json. +func (o OptNilUUID) Encode(e *jx.Encoder) { if !o.Set { return } @@ -9267,20 +10518,20 @@ func (o OptNilDateTime) Encode(e *jx.Encoder, format func(*jx.Encoder, time.Time e.Null() return } - format(e, o.Value) + json.EncodeUUID(e, o.Value) } -// Decode decodes time.Time from json. -func (o *OptNilDateTime) Decode(d *jx.Decoder, format func(*jx.Decoder) (time.Time, error)) error { +// Decode decodes uuid.UUID from json. +func (o *OptNilUUID) Decode(d *jx.Decoder) error { if o == nil { - return errors.New("invalid: unable to decode OptNilDateTime to nil") + return errors.New("invalid: unable to decode OptNilUUID to nil") } if d.Next() == jx.Null { if err := d.Null(); err != nil { return err } - var v time.Time + var v uuid.UUID o.Value = v o.Set = true o.Null = true @@ -9288,7 +10539,7 @@ func (o *OptNilDateTime) Decode(d *jx.Decoder, format func(*jx.Decoder) (time.Ti } o.Set = true o.Null = false - v, err := format(d) + v, err := json.DecodeUUID(d) if err != nil { return err } @@ -9297,48 +10548,32 @@ func (o *OptNilDateTime) Decode(d *jx.Decoder, format func(*jx.Decoder) (time.Ti } // MarshalJSON implements stdjson.Marshaler. -func (s OptNilDateTime) MarshalJSON() ([]byte, error) { +func (s OptNilUUID) MarshalJSON() ([]byte, error) { e := jx.Encoder{} - s.Encode(&e, json.EncodeDateTime) + s.Encode(&e) return e.Bytes(), nil } // UnmarshalJSON implements stdjson.Unmarshaler. -func (s *OptNilDateTime) UnmarshalJSON(data []byte) error { +func (s *OptNilUUID) UnmarshalJSON(data []byte) error { d := jx.DecodeBytes(data) - return s.Decode(d, json.DecodeDateTime) + return s.Decode(d) } -// Encode encodes DeliverySummaryEmail as json. -func (o OptNilDeliverySummaryEmail) Encode(e *jx.Encoder) { +// Encode encodes SentEmailStatus as json. +func (o OptSentEmailStatus) Encode(e *jx.Encoder) { if !o.Set { return } - if o.Null { - e.Null() - return - } - o.Value.Encode(e) + e.Str(string(o.Value)) } -// Decode decodes DeliverySummaryEmail from json. -func (o *OptNilDeliverySummaryEmail) Decode(d *jx.Decoder) error { +// Decode decodes SentEmailStatus from json. +func (o *OptSentEmailStatus) Decode(d *jx.Decoder) error { if o == nil { - return errors.New("invalid: unable to decode OptNilDeliverySummaryEmail to nil") - } - if d.Next() == jx.Null { - if err := d.Null(); err != nil { - return err - } - - var v DeliverySummaryEmail - o.Value = v - o.Set = true - o.Null = true - return nil + return errors.New("invalid: unable to decode OptSentEmailStatus to nil") } o.Set = true - o.Null = false if err := o.Value.Decode(d); err != nil { return err } @@ -9346,48 +10581,32 @@ func (o *OptNilDeliverySummaryEmail) Decode(d *jx.Decoder) error { } // MarshalJSON implements stdjson.Marshaler. -func (s OptNilDeliverySummaryEmail) MarshalJSON() ([]byte, error) { +func (s OptSentEmailStatus) MarshalJSON() ([]byte, error) { e := jx.Encoder{} s.Encode(&e) return e.Bytes(), nil } // UnmarshalJSON implements stdjson.Unmarshaler. -func (s *OptNilDeliverySummaryEmail) UnmarshalJSON(data []byte) error { +func (s *OptSentEmailStatus) UnmarshalJSON(data []byte) error { d := jx.DecodeBytes(data) return s.Decode(d) } -// Encode encodes EmailWebhookStatus as json. -func (o OptNilEmailWebhookStatus) Encode(e *jx.Encoder) { +// Encode encodes StartCliLoginInput as json. +func (o OptStartCliLoginInput) Encode(e *jx.Encoder) { if !o.Set { return } - if o.Null { - e.Null() - return - } - e.Str(string(o.Value)) + o.Value.Encode(e) } -// Decode decodes EmailWebhookStatus from json. -func (o *OptNilEmailWebhookStatus) Decode(d *jx.Decoder) error { +// Decode decodes StartCliLoginInput from json. +func (o *OptStartCliLoginInput) Decode(d *jx.Decoder) error { if o == nil { - return errors.New("invalid: unable to decode OptNilEmailWebhookStatus to nil") - } - if d.Next() == jx.Null { - if err := d.Null(); err != nil { - return err - } - - var v EmailWebhookStatus - o.Value = v - o.Set = true - o.Null = true - return nil + return errors.New("invalid: unable to decode OptStartCliLoginInput to nil") } o.Set = true - o.Null = false if err := o.Value.Decode(d); err != nil { return err } @@ -9395,497 +10614,488 @@ func (o *OptNilEmailWebhookStatus) Decode(d *jx.Decoder) error { } // MarshalJSON implements stdjson.Marshaler. -func (s OptNilEmailWebhookStatus) MarshalJSON() ([]byte, error) { +func (s OptStartCliLoginInput) MarshalJSON() ([]byte, error) { e := jx.Encoder{} s.Encode(&e) return e.Bytes(), nil } // UnmarshalJSON implements stdjson.Unmarshaler. -func (s *OptNilEmailWebhookStatus) UnmarshalJSON(data []byte) error { +func (s *OptStartCliLoginInput) UnmarshalJSON(data []byte) error { d := jx.DecodeBytes(data) return s.Decode(d) } -// Encode encodes float64 as json. -func (o OptNilFloat64) Encode(e *jx.Encoder) { +// Encode encodes StartCliLoginInputMetadata as json. +func (o OptStartCliLoginInputMetadata) Encode(e *jx.Encoder) { if !o.Set { return } - if o.Null { - e.Null() - return - } - e.Float64(float64(o.Value)) + o.Value.Encode(e) } -// Decode decodes float64 from json. -func (o *OptNilFloat64) Decode(d *jx.Decoder) error { +// Decode decodes StartCliLoginInputMetadata from json. +func (o *OptStartCliLoginInputMetadata) Decode(d *jx.Decoder) error { if o == nil { - return errors.New("invalid: unable to decode OptNilFloat64 to nil") - } - if d.Next() == jx.Null { - if err := d.Null(); err != nil { - return err - } - - var v float64 - o.Value = v - o.Set = true - o.Null = true - return nil + return errors.New("invalid: unable to decode OptStartCliLoginInputMetadata to nil") } o.Set = true - o.Null = false - v, err := d.Float64() - if err != nil { + o.Value = make(StartCliLoginInputMetadata) + if err := o.Value.Decode(d); err != nil { return err } - o.Value = float64(v) return nil } // MarshalJSON implements stdjson.Marshaler. -func (s OptNilFloat64) MarshalJSON() ([]byte, error) { +func (s OptStartCliLoginInputMetadata) MarshalJSON() ([]byte, error) { e := jx.Encoder{} s.Encode(&e) return e.Bytes(), nil } // UnmarshalJSON implements stdjson.Unmarshaler. -func (s *OptNilFloat64) UnmarshalJSON(data []byte) error { +func (s *OptStartCliLoginInputMetadata) UnmarshalJSON(data []byte) error { d := jx.DecodeBytes(data) return s.Decode(d) } -// Encode encodes []GateDenial as json. -func (o OptNilGateDenialArray) Encode(e *jx.Encoder) { +// Encode encodes string as json. +func (o OptString) Encode(e *jx.Encoder) { if !o.Set { return } - if o.Null { - e.Null() - return - } - e.ArrStart() - for _, elem := range o.Value { - elem.Encode(e) - } - e.ArrEnd() + e.Str(string(o.Value)) } -// Decode decodes []GateDenial from json. -func (o *OptNilGateDenialArray) Decode(d *jx.Decoder) error { +// Decode decodes string from json. +func (o *OptString) Decode(d *jx.Decoder) error { if o == nil { - return errors.New("invalid: unable to decode OptNilGateDenialArray to nil") - } - if d.Next() == jx.Null { - if err := d.Null(); err != nil { - return err - } - - var v []GateDenial - o.Value = v - o.Set = true - o.Null = true - return nil + return errors.New("invalid: unable to decode OptString to nil") } o.Set = true - o.Null = false - o.Value = make([]GateDenial, 0) - if err := d.Arr(func(d *jx.Decoder) error { - var elem GateDenial - if err := elem.Decode(d); err != nil { - return err - } - o.Value = append(o.Value, elem) - return nil - }); err != nil { + v, err := d.Str() + if err != nil { return err } + o.Value = string(v) return nil } // MarshalJSON implements stdjson.Marshaler. -func (s OptNilGateDenialArray) MarshalJSON() ([]byte, error) { +func (s OptString) MarshalJSON() ([]byte, error) { e := jx.Encoder{} s.Encode(&e) return e.Bytes(), nil } // UnmarshalJSON implements stdjson.Unmarshaler. -func (s *OptNilGateDenialArray) UnmarshalJSON(data []byte) error { +func (s *OptString) UnmarshalJSON(data []byte) error { d := jx.DecodeBytes(data) return s.Decode(d) } -// Encode encodes int as json. -func (o OptNilInt) Encode(e *jx.Encoder) { +// Encode encodes uuid.UUID as json. +func (o OptUUID) Encode(e *jx.Encoder) { if !o.Set { return } - if o.Null { - e.Null() - return - } - e.Int(int(o.Value)) + json.EncodeUUID(e, o.Value) } -// Decode decodes int from json. -func (o *OptNilInt) Decode(d *jx.Decoder) error { +// Decode decodes uuid.UUID from json. +func (o *OptUUID) Decode(d *jx.Decoder) error { if o == nil { - return errors.New("invalid: unable to decode OptNilInt to nil") - } - if d.Next() == jx.Null { - if err := d.Null(); err != nil { - return err - } - - var v int - o.Value = v - o.Set = true - o.Null = true - return nil + return errors.New("invalid: unable to decode OptUUID to nil") } o.Set = true - o.Null = false - v, err := d.Int() + v, err := json.DecodeUUID(d) if err != nil { return err } - o.Value = int(v) + o.Value = v return nil } // MarshalJSON implements stdjson.Marshaler. -func (s OptNilInt) MarshalJSON() ([]byte, error) { +func (s OptUUID) MarshalJSON() ([]byte, error) { e := jx.Encoder{} s.Encode(&e) return e.Bytes(), nil } // UnmarshalJSON implements stdjson.Unmarshaler. -func (s *OptNilInt) UnmarshalJSON(data []byte) error { +func (s *OptUUID) UnmarshalJSON(data []byte) error { d := jx.DecodeBytes(data) return s.Decode(d) } -// Encode encodes string as json. -func (o OptNilString) Encode(e *jx.Encoder) { - if !o.Set { - return +// Encode implements json.Marshaler. +func (s *PaginationMeta) Encode(e *jx.Encoder) { + e.ObjStart() + s.encodeFields(e) + e.ObjEnd() +} + +// encodeFields encodes fields. +func (s *PaginationMeta) encodeFields(e *jx.Encoder) { + { + e.FieldStart("total") + e.Int(s.Total) } - if o.Null { - e.Null() - return + { + e.FieldStart("limit") + e.Int(s.Limit) + } + { + e.FieldStart("cursor") + s.Cursor.Encode(e) } - e.Str(string(o.Value)) } -// Decode decodes string from json. -func (o *OptNilString) Decode(d *jx.Decoder) error { - if o == nil { - return errors.New("invalid: unable to decode OptNilString to nil") +var jsonFieldsNameOfPaginationMeta = [3]string{ + 0: "total", + 1: "limit", + 2: "cursor", +} + +// Decode decodes PaginationMeta from json. +func (s *PaginationMeta) Decode(d *jx.Decoder) error { + if s == nil { + return errors.New("invalid: unable to decode PaginationMeta to nil") } - if d.Next() == jx.Null { - if err := d.Null(); err != nil { - return err + var requiredBitSet [1]uint8 + + if err := d.ObjBytes(func(d *jx.Decoder, k []byte) error { + switch string(k) { + case "total": + requiredBitSet[0] |= 1 << 0 + if err := func() error { + v, err := d.Int() + s.Total = int(v) + if err != nil { + return err + } + return nil + }(); err != nil { + return errors.Wrap(err, "decode field \"total\"") + } + case "limit": + requiredBitSet[0] |= 1 << 1 + if err := func() error { + v, err := d.Int() + s.Limit = int(v) + if err != nil { + return err + } + return nil + }(); err != nil { + return errors.Wrap(err, "decode field \"limit\"") + } + case "cursor": + requiredBitSet[0] |= 1 << 2 + if err := func() error { + if err := s.Cursor.Decode(d); err != nil { + return err + } + return nil + }(); err != nil { + return errors.Wrap(err, "decode field \"cursor\"") + } + default: + return d.Skip() } - - var v string - o.Value = v - o.Set = true - o.Null = true return nil + }); err != nil { + return errors.Wrap(err, "decode PaginationMeta") } - o.Set = true - o.Null = false - v, err := d.Str() - if err != nil { - return err + // Validate required fields. + var failures []validate.FieldError + for i, mask := range [1]uint8{ + 0b00000111, + } { + if result := (requiredBitSet[i] & mask) ^ mask; result != 0 { + // Mask only required fields and check equality to mask using XOR. + // + // If XOR result is not zero, result is not equal to expected, so some fields are missed. + // Bits of fields which would be set are actually bits of missed fields. + missed := bits.OnesCount8(result) + for bitN := 0; bitN < missed; bitN++ { + bitIdx := bits.TrailingZeros8(result) + fieldIdx := i*8 + bitIdx + var name string + if fieldIdx < len(jsonFieldsNameOfPaginationMeta) { + name = jsonFieldsNameOfPaginationMeta[fieldIdx] + } else { + name = strconv.Itoa(fieldIdx) + } + failures = append(failures, validate.FieldError{ + Name: name, + Error: validate.ErrFieldRequired, + }) + // Reset bit. + result &^= 1 << bitIdx + } + } } - o.Value = string(v) + if len(failures) > 0 { + return &validate.Error{Fields: failures} + } + return nil } // MarshalJSON implements stdjson.Marshaler. -func (s OptNilString) MarshalJSON() ([]byte, error) { +func (s *PaginationMeta) MarshalJSON() ([]byte, error) { e := jx.Encoder{} s.Encode(&e) return e.Bytes(), nil } // UnmarshalJSON implements stdjson.Unmarshaler. -func (s *OptNilString) UnmarshalJSON(data []byte) error { +func (s *PaginationMeta) UnmarshalJSON(data []byte) error { d := jx.DecodeBytes(data) return s.Decode(d) } -// Encode encodes []string as json. -func (o OptNilStringArray) Encode(e *jx.Encoder) { - if !o.Set { - return - } - if o.Null { - e.Null() - return - } - e.ArrStart() - for _, elem := range o.Value { - e.Str(elem) - } - e.ArrEnd() -} +// Encode encodes PollCliLoginBadRequest as json. +func (s *PollCliLoginBadRequest) Encode(e *jx.Encoder) { + unwrapped := (*ErrorResponse)(s) -// Decode decodes []string from json. -func (o *OptNilStringArray) Decode(d *jx.Decoder) error { - if o == nil { - return errors.New("invalid: unable to decode OptNilStringArray to nil") - } - if d.Next() == jx.Null { - if err := d.Null(); err != nil { - return err - } + unwrapped.Encode(e) +} - var v []string - o.Value = v - o.Set = true - o.Null = true - return nil +// Decode decodes PollCliLoginBadRequest from json. +func (s *PollCliLoginBadRequest) Decode(d *jx.Decoder) error { + if s == nil { + return errors.New("invalid: unable to decode PollCliLoginBadRequest to nil") } - o.Set = true - o.Null = false - o.Value = make([]string, 0) - if err := d.Arr(func(d *jx.Decoder) error { - var elem string - v, err := d.Str() - elem = string(v) - if err != nil { + var unwrapped ErrorResponse + if err := func() error { + if err := unwrapped.Decode(d); err != nil { return err } - o.Value = append(o.Value, elem) return nil - }); err != nil { - return err + }(); err != nil { + return errors.Wrap(err, "alias") } + *s = PollCliLoginBadRequest(unwrapped) return nil } // MarshalJSON implements stdjson.Marshaler. -func (s OptNilStringArray) MarshalJSON() ([]byte, error) { +func (s *PollCliLoginBadRequest) MarshalJSON() ([]byte, error) { e := jx.Encoder{} s.Encode(&e) return e.Bytes(), nil } // UnmarshalJSON implements stdjson.Unmarshaler. -func (s *OptNilStringArray) UnmarshalJSON(data []byte) error { +func (s *PollCliLoginBadRequest) UnmarshalJSON(data []byte) error { d := jx.DecodeBytes(data) return s.Decode(d) } -// Encode encodes uuid.UUID as json. -func (o OptNilUUID) Encode(e *jx.Encoder) { - if !o.Set { - return - } - if o.Null { - e.Null() - return - } - json.EncodeUUID(e, o.Value) +// Encode encodes PollCliLoginForbidden as json. +func (s *PollCliLoginForbidden) Encode(e *jx.Encoder) { + unwrapped := (*ErrorResponse)(s) + + unwrapped.Encode(e) } -// Decode decodes uuid.UUID from json. -func (o *OptNilUUID) Decode(d *jx.Decoder) error { - if o == nil { - return errors.New("invalid: unable to decode OptNilUUID to nil") +// Decode decodes PollCliLoginForbidden from json. +func (s *PollCliLoginForbidden) Decode(d *jx.Decoder) error { + if s == nil { + return errors.New("invalid: unable to decode PollCliLoginForbidden to nil") } - if d.Next() == jx.Null { - if err := d.Null(); err != nil { + var unwrapped ErrorResponse + if err := func() error { + if err := unwrapped.Decode(d); err != nil { return err } - - var v uuid.UUID - o.Value = v - o.Set = true - o.Null = true return nil + }(); err != nil { + return errors.Wrap(err, "alias") } - o.Set = true - o.Null = false - v, err := json.DecodeUUID(d) - if err != nil { - return err - } - o.Value = v + *s = PollCliLoginForbidden(unwrapped) return nil } // MarshalJSON implements stdjson.Marshaler. -func (s OptNilUUID) MarshalJSON() ([]byte, error) { +func (s *PollCliLoginForbidden) MarshalJSON() ([]byte, error) { e := jx.Encoder{} s.Encode(&e) return e.Bytes(), nil } // UnmarshalJSON implements stdjson.Unmarshaler. -func (s *OptNilUUID) UnmarshalJSON(data []byte) error { +func (s *PollCliLoginForbidden) UnmarshalJSON(data []byte) error { d := jx.DecodeBytes(data) return s.Decode(d) } -// Encode encodes SentEmailStatus as json. -func (o OptSentEmailStatus) Encode(e *jx.Encoder) { - if !o.Set { - return - } - e.Str(string(o.Value)) +// Encode implements json.Marshaler. +func (s *PollCliLoginInput) Encode(e *jx.Encoder) { + e.ObjStart() + s.encodeFields(e) + e.ObjEnd() } -// Decode decodes SentEmailStatus from json. -func (o *OptSentEmailStatus) Decode(d *jx.Decoder) error { - if o == nil { - return errors.New("invalid: unable to decode OptSentEmailStatus to nil") - } - o.Set = true - if err := o.Value.Decode(d); err != nil { - return err +// encodeFields encodes fields. +func (s *PollCliLoginInput) encodeFields(e *jx.Encoder) { + { + e.FieldStart("device_code") + e.Str(s.DeviceCode) } - return nil -} - -// MarshalJSON implements stdjson.Marshaler. -func (s OptSentEmailStatus) MarshalJSON() ([]byte, error) { - e := jx.Encoder{} - s.Encode(&e) - return e.Bytes(), nil } -// UnmarshalJSON implements stdjson.Unmarshaler. -func (s *OptSentEmailStatus) UnmarshalJSON(data []byte) error { - d := jx.DecodeBytes(data) - return s.Decode(d) +var jsonFieldsNameOfPollCliLoginInput = [1]string{ + 0: "device_code", } -// Encode encodes string as json. -func (o OptString) Encode(e *jx.Encoder) { - if !o.Set { - return +// Decode decodes PollCliLoginInput from json. +func (s *PollCliLoginInput) Decode(d *jx.Decoder) error { + if s == nil { + return errors.New("invalid: unable to decode PollCliLoginInput to nil") } - e.Str(string(o.Value)) -} + var requiredBitSet [1]uint8 -// Decode decodes string from json. -func (o *OptString) Decode(d *jx.Decoder) error { - if o == nil { - return errors.New("invalid: unable to decode OptString to nil") + if err := d.ObjBytes(func(d *jx.Decoder, k []byte) error { + switch string(k) { + case "device_code": + requiredBitSet[0] |= 1 << 0 + if err := func() error { + v, err := d.Str() + s.DeviceCode = string(v) + if err != nil { + return err + } + return nil + }(); err != nil { + return errors.Wrap(err, "decode field \"device_code\"") + } + default: + return errors.Errorf("unexpected field %q", k) + } + return nil + }); err != nil { + return errors.Wrap(err, "decode PollCliLoginInput") } - o.Set = true - v, err := d.Str() - if err != nil { - return err + // Validate required fields. + var failures []validate.FieldError + for i, mask := range [1]uint8{ + 0b00000001, + } { + if result := (requiredBitSet[i] & mask) ^ mask; result != 0 { + // Mask only required fields and check equality to mask using XOR. + // + // If XOR result is not zero, result is not equal to expected, so some fields are missed. + // Bits of fields which would be set are actually bits of missed fields. + missed := bits.OnesCount8(result) + for bitN := 0; bitN < missed; bitN++ { + bitIdx := bits.TrailingZeros8(result) + fieldIdx := i*8 + bitIdx + var name string + if fieldIdx < len(jsonFieldsNameOfPollCliLoginInput) { + name = jsonFieldsNameOfPollCliLoginInput[fieldIdx] + } else { + name = strconv.Itoa(fieldIdx) + } + failures = append(failures, validate.FieldError{ + Name: name, + Error: validate.ErrFieldRequired, + }) + // Reset bit. + result &^= 1 << bitIdx + } + } } - o.Value = string(v) + if len(failures) > 0 { + return &validate.Error{Fields: failures} + } + return nil } // MarshalJSON implements stdjson.Marshaler. -func (s OptString) MarshalJSON() ([]byte, error) { +func (s *PollCliLoginInput) MarshalJSON() ([]byte, error) { e := jx.Encoder{} s.Encode(&e) return e.Bytes(), nil } // UnmarshalJSON implements stdjson.Unmarshaler. -func (s *OptString) UnmarshalJSON(data []byte) error { +func (s *PollCliLoginInput) UnmarshalJSON(data []byte) error { d := jx.DecodeBytes(data) return s.Decode(d) } // Encode implements json.Marshaler. -func (s *PaginationMeta) Encode(e *jx.Encoder) { +func (s *PollCliLoginOK) Encode(e *jx.Encoder) { e.ObjStart() s.encodeFields(e) e.ObjEnd() } -// encodeFields encodes fields. -func (s *PaginationMeta) encodeFields(e *jx.Encoder) { - { - e.FieldStart("total") - e.Int(s.Total) - } +// encodeFields encodes fields. +func (s *PollCliLoginOK) encodeFields(e *jx.Encoder) { { - e.FieldStart("limit") - e.Int(s.Limit) + e.FieldStart("success") + e.Bool(true) } { - e.FieldStart("cursor") - s.Cursor.Encode(e) + e.FieldStart("data") + s.Data.Encode(e) } } -var jsonFieldsNameOfPaginationMeta = [3]string{ - 0: "total", - 1: "limit", - 2: "cursor", +var jsonFieldsNameOfPollCliLoginOK = [2]string{ + 0: "success", + 1: "data", } -// Decode decodes PaginationMeta from json. -func (s *PaginationMeta) Decode(d *jx.Decoder) error { +// Decode decodes PollCliLoginOK from json. +func (s *PollCliLoginOK) Decode(d *jx.Decoder) error { if s == nil { - return errors.New("invalid: unable to decode PaginationMeta to nil") + return errors.New("invalid: unable to decode PollCliLoginOK to nil") } var requiredBitSet [1]uint8 if err := d.ObjBytes(func(d *jx.Decoder, k []byte) error { switch string(k) { - case "total": + case "success": requiredBitSet[0] |= 1 << 0 if err := func() error { - v, err := d.Int() - s.Total = int(v) + v, err := d.Bool() + s.Success = bool(v) if err != nil { return err } return nil }(); err != nil { - return errors.Wrap(err, "decode field \"total\"") + return errors.Wrap(err, "decode field \"success\"") } - case "limit": + case "data": requiredBitSet[0] |= 1 << 1 if err := func() error { - v, err := d.Int() - s.Limit = int(v) - if err != nil { - return err - } - return nil - }(); err != nil { - return errors.Wrap(err, "decode field \"limit\"") - } - case "cursor": - requiredBitSet[0] |= 1 << 2 - if err := func() error { - if err := s.Cursor.Decode(d); err != nil { + if err := s.Data.Decode(d); err != nil { return err } return nil }(); err != nil { - return errors.Wrap(err, "decode field \"cursor\"") + return errors.Wrap(err, "decode field \"data\"") } default: return d.Skip() } return nil }); err != nil { - return errors.Wrap(err, "decode PaginationMeta") + return errors.Wrap(err, "decode PollCliLoginOK") } // Validate required fields. var failures []validate.FieldError for i, mask := range [1]uint8{ - 0b00000111, + 0b00000011, } { if result := (requiredBitSet[i] & mask) ^ mask; result != 0 { // Mask only required fields and check equality to mask using XOR. @@ -9897,8 +11107,8 @@ func (s *PaginationMeta) Decode(d *jx.Decoder) error { bitIdx := bits.TrailingZeros8(result) fieldIdx := i*8 + bitIdx var name string - if fieldIdx < len(jsonFieldsNameOfPaginationMeta) { - name = jsonFieldsNameOfPaginationMeta[fieldIdx] + if fieldIdx < len(jsonFieldsNameOfPollCliLoginOK) { + name = jsonFieldsNameOfPollCliLoginOK[fieldIdx] } else { name = strconv.Itoa(fieldIdx) } @@ -9919,14 +11129,14 @@ func (s *PaginationMeta) Decode(d *jx.Decoder) error { } // MarshalJSON implements stdjson.Marshaler. -func (s *PaginationMeta) MarshalJSON() ([]byte, error) { +func (s *PollCliLoginOK) MarshalJSON() ([]byte, error) { e := jx.Encoder{} s.Encode(&e) return e.Bytes(), nil } // UnmarshalJSON implements stdjson.Unmarshaler. -func (s *PaginationMeta) UnmarshalJSON(data []byte) error { +func (s *PollCliLoginOK) UnmarshalJSON(data []byte) error { d := jx.DecodeBytes(data) return s.Decode(d) } @@ -14304,6 +15514,255 @@ func (s *SentEmailSummary) UnmarshalJSON(data []byte) error { return s.Decode(d) } +// Encode implements json.Marshaler. +func (s *StartCliLoginCreated) Encode(e *jx.Encoder) { + e.ObjStart() + s.encodeFields(e) + e.ObjEnd() +} + +// encodeFields encodes fields. +func (s *StartCliLoginCreated) encodeFields(e *jx.Encoder) { + { + e.FieldStart("success") + e.Bool(true) + } + { + e.FieldStart("data") + s.Data.Encode(e) + } +} + +var jsonFieldsNameOfStartCliLoginCreated = [2]string{ + 0: "success", + 1: "data", +} + +// Decode decodes StartCliLoginCreated from json. +func (s *StartCliLoginCreated) Decode(d *jx.Decoder) error { + if s == nil { + return errors.New("invalid: unable to decode StartCliLoginCreated to nil") + } + var requiredBitSet [1]uint8 + + if err := d.ObjBytes(func(d *jx.Decoder, k []byte) error { + switch string(k) { + case "success": + requiredBitSet[0] |= 1 << 0 + if err := func() error { + v, err := d.Bool() + s.Success = bool(v) + if err != nil { + return err + } + return nil + }(); err != nil { + return errors.Wrap(err, "decode field \"success\"") + } + case "data": + requiredBitSet[0] |= 1 << 1 + if err := func() error { + if err := s.Data.Decode(d); err != nil { + return err + } + return nil + }(); err != nil { + return errors.Wrap(err, "decode field \"data\"") + } + default: + return d.Skip() + } + return nil + }); err != nil { + return errors.Wrap(err, "decode StartCliLoginCreated") + } + // Validate required fields. + var failures []validate.FieldError + for i, mask := range [1]uint8{ + 0b00000011, + } { + if result := (requiredBitSet[i] & mask) ^ mask; result != 0 { + // Mask only required fields and check equality to mask using XOR. + // + // If XOR result is not zero, result is not equal to expected, so some fields are missed. + // Bits of fields which would be set are actually bits of missed fields. + missed := bits.OnesCount8(result) + for bitN := 0; bitN < missed; bitN++ { + bitIdx := bits.TrailingZeros8(result) + fieldIdx := i*8 + bitIdx + var name string + if fieldIdx < len(jsonFieldsNameOfStartCliLoginCreated) { + name = jsonFieldsNameOfStartCliLoginCreated[fieldIdx] + } else { + name = strconv.Itoa(fieldIdx) + } + failures = append(failures, validate.FieldError{ + Name: name, + Error: validate.ErrFieldRequired, + }) + // Reset bit. + result &^= 1 << bitIdx + } + } + } + if len(failures) > 0 { + return &validate.Error{Fields: failures} + } + + return nil +} + +// MarshalJSON implements stdjson.Marshaler. +func (s *StartCliLoginCreated) MarshalJSON() ([]byte, error) { + e := jx.Encoder{} + s.Encode(&e) + return e.Bytes(), nil +} + +// UnmarshalJSON implements stdjson.Unmarshaler. +func (s *StartCliLoginCreated) UnmarshalJSON(data []byte) error { + d := jx.DecodeBytes(data) + return s.Decode(d) +} + +// Encode implements json.Marshaler. +func (s *StartCliLoginInput) Encode(e *jx.Encoder) { + e.ObjStart() + s.encodeFields(e) + e.ObjEnd() +} + +// encodeFields encodes fields. +func (s *StartCliLoginInput) encodeFields(e *jx.Encoder) { + { + if s.DeviceName.Set { + e.FieldStart("device_name") + s.DeviceName.Encode(e) + } + } + { + if s.Metadata.Set { + e.FieldStart("metadata") + s.Metadata.Encode(e) + } + } +} + +var jsonFieldsNameOfStartCliLoginInput = [2]string{ + 0: "device_name", + 1: "metadata", +} + +// Decode decodes StartCliLoginInput from json. +func (s *StartCliLoginInput) Decode(d *jx.Decoder) error { + if s == nil { + return errors.New("invalid: unable to decode StartCliLoginInput to nil") + } + + if err := d.ObjBytes(func(d *jx.Decoder, k []byte) error { + switch string(k) { + case "device_name": + if err := func() error { + s.DeviceName.Reset() + if err := s.DeviceName.Decode(d); err != nil { + return err + } + return nil + }(); err != nil { + return errors.Wrap(err, "decode field \"device_name\"") + } + case "metadata": + if err := func() error { + s.Metadata.Reset() + if err := s.Metadata.Decode(d); err != nil { + return err + } + return nil + }(); err != nil { + return errors.Wrap(err, "decode field \"metadata\"") + } + default: + return errors.Errorf("unexpected field %q", k) + } + return nil + }); err != nil { + return errors.Wrap(err, "decode StartCliLoginInput") + } + + return nil +} + +// MarshalJSON implements stdjson.Marshaler. +func (s *StartCliLoginInput) MarshalJSON() ([]byte, error) { + e := jx.Encoder{} + s.Encode(&e) + return e.Bytes(), nil +} + +// UnmarshalJSON implements stdjson.Unmarshaler. +func (s *StartCliLoginInput) UnmarshalJSON(data []byte) error { + d := jx.DecodeBytes(data) + return s.Decode(d) +} + +// Encode implements json.Marshaler. +func (s StartCliLoginInputMetadata) Encode(e *jx.Encoder) { + e.ObjStart() + s.encodeFields(e) + e.ObjEnd() +} + +// encodeFields implements json.Marshaler. +func (s StartCliLoginInputMetadata) encodeFields(e *jx.Encoder) { + for k, elem := range s { + e.FieldStart(k) + + if len(elem) != 0 { + e.Raw(elem) + } + } +} + +// Decode decodes StartCliLoginInputMetadata from json. +func (s *StartCliLoginInputMetadata) Decode(d *jx.Decoder) error { + if s == nil { + return errors.New("invalid: unable to decode StartCliLoginInputMetadata to nil") + } + m := s.init() + if err := d.ObjBytes(func(d *jx.Decoder, k []byte) error { + var elem jx.Raw + if err := func() error { + v, err := d.RawAppend(nil) + elem = jx.Raw(v) + if err != nil { + return err + } + return nil + }(); err != nil { + return errors.Wrapf(err, "decode field %q", k) + } + m[string(k)] = elem + return nil + }); err != nil { + return errors.Wrap(err, "decode StartCliLoginInputMetadata") + } + + return nil +} + +// MarshalJSON implements stdjson.Marshaler. +func (s StartCliLoginInputMetadata) MarshalJSON() ([]byte, error) { + e := jx.Encoder{} + s.Encode(&e) + return e.Bytes(), nil +} + +// UnmarshalJSON implements stdjson.Unmarshaler. +func (s *StartCliLoginInputMetadata) UnmarshalJSON(data []byte) error { + d := jx.DecodeBytes(data) + return s.Decode(d) +} + // Encode implements json.Marshaler. func (s *StorageStats) Encode(e *jx.Encoder) { e.ObjStart() diff --git a/sdk-go/api/oas_operations_gen.go b/sdk-go/api/oas_operations_gen.go index f948004..f5e9087 100644 --- a/sdk-go/api/oas_operations_gen.go +++ b/sdk-go/api/oas_operations_gen.go @@ -7,6 +7,7 @@ type OperationName = string const ( AddDomainOperation OperationName = "AddDomain" + CliLogoutOperation OperationName = "CliLogout" CreateEndpointOperation OperationName = "CreateEndpoint" CreateFilterOperation OperationName = "CreateFilter" DeleteDomainOperation OperationName = "DeleteDomain" @@ -28,11 +29,13 @@ const ( ListEndpointsOperation OperationName = "ListEndpoints" ListFiltersOperation OperationName = "ListFilters" ListSentEmailsOperation OperationName = "ListSentEmails" + PollCliLoginOperation OperationName = "PollCliLogin" ReplayDeliveryOperation OperationName = "ReplayDelivery" ReplayEmailWebhooksOperation OperationName = "ReplayEmailWebhooks" ReplyToEmailOperation OperationName = "ReplyToEmail" RotateWebhookSecretOperation OperationName = "RotateWebhookSecret" SendEmailOperation OperationName = "SendEmail" + StartCliLoginOperation OperationName = "StartCliLogin" TestEndpointOperation OperationName = "TestEndpoint" UpdateAccountOperation OperationName = "UpdateAccount" UpdateDomainOperation OperationName = "UpdateDomain" diff --git a/sdk-go/api/oas_request_decoders_gen.go b/sdk-go/api/oas_request_decoders_gen.go index d0e0c34..4fddeb0 100644 --- a/sdk-go/api/oas_request_decoders_gen.go +++ b/sdk-go/api/oas_request_decoders_gen.go @@ -93,6 +93,81 @@ func (s *Server) decodeAddDomainRequest(r *http.Request) ( } } +func (s *Server) decodeCliLogoutRequest(r *http.Request) ( + req OptCliLogoutInput, + rawBody []byte, + close func() error, + rerr error, +) { + var closers []func() error + close = func() error { + var merr error + // Close in reverse order, to match defer behavior. + for i := len(closers) - 1; i >= 0; i-- { + c := closers[i] + merr = errors.Join(merr, c()) + } + return merr + } + defer func() { + if rerr != nil { + rerr = errors.Join(rerr, close()) + } + }() + if _, ok := r.Header["Content-Type"]; !ok && r.ContentLength == 0 { + return req, rawBody, close, nil + } + ct, _, err := mime.ParseMediaType(r.Header.Get("Content-Type")) + if err != nil { + return req, rawBody, close, errors.Wrap(err, "parse media type") + } + switch { + case ct == "application/json": + if r.ContentLength == 0 { + return req, rawBody, close, nil + } + buf, err := io.ReadAll(r.Body) + defer func() { + _ = r.Body.Close() + }() + if err != nil { + return req, rawBody, close, err + } + + // Reset the body to allow for downstream reading. + r.Body = io.NopCloser(bytes.NewBuffer(buf)) + + if len(buf) == 0 { + return req, rawBody, close, nil + } + + rawBody = append(rawBody, buf...) + d := jx.DecodeBytes(buf) + + var request OptCliLogoutInput + if err := func() error { + request.Reset() + if err := request.Decode(d); err != nil { + return err + } + if err := d.Skip(); err != io.EOF { + return errors.New("unexpected trailing data") + } + return nil + }(); err != nil { + err = &ogenerrors.DecodeBodyError{ + ContentType: ct, + Body: buf, + Err: err, + } + return req, rawBody, close, err + } + return request, rawBody, close, nil + default: + return req, rawBody, close, validate.InvalidContentType(ct) + } +} + func (s *Server) decodeCreateEndpointRequest(r *http.Request) ( req *CreateEndpointInput, rawBody []byte, @@ -251,6 +326,85 @@ func (s *Server) decodeCreateFilterRequest(r *http.Request) ( } } +func (s *Server) decodePollCliLoginRequest(r *http.Request) ( + req *PollCliLoginInput, + rawBody []byte, + close func() error, + rerr error, +) { + var closers []func() error + close = func() error { + var merr error + // Close in reverse order, to match defer behavior. + for i := len(closers) - 1; i >= 0; i-- { + c := closers[i] + merr = errors.Join(merr, c()) + } + return merr + } + defer func() { + if rerr != nil { + rerr = errors.Join(rerr, close()) + } + }() + ct, _, err := mime.ParseMediaType(r.Header.Get("Content-Type")) + if err != nil { + return req, rawBody, close, errors.Wrap(err, "parse media type") + } + switch { + case ct == "application/json": + if r.ContentLength == 0 { + return req, rawBody, close, validate.ErrBodyRequired + } + buf, err := io.ReadAll(r.Body) + defer func() { + _ = r.Body.Close() + }() + if err != nil { + return req, rawBody, close, err + } + + // Reset the body to allow for downstream reading. + r.Body = io.NopCloser(bytes.NewBuffer(buf)) + + if len(buf) == 0 { + return req, rawBody, close, validate.ErrBodyRequired + } + + rawBody = append(rawBody, buf...) + d := jx.DecodeBytes(buf) + + var request PollCliLoginInput + if err := func() error { + if err := request.Decode(d); err != nil { + return err + } + if err := d.Skip(); err != io.EOF { + return errors.New("unexpected trailing data") + } + return nil + }(); err != nil { + err = &ogenerrors.DecodeBodyError{ + ContentType: ct, + Body: buf, + Err: err, + } + return req, rawBody, close, err + } + if err := func() error { + if err := request.Validate(); err != nil { + return err + } + return nil + }(); err != nil { + return req, rawBody, close, errors.Wrap(err, "validate") + } + return &request, rawBody, close, nil + default: + return req, rawBody, close, validate.InvalidContentType(ct) + } +} + func (s *Server) decodeReplyToEmailRequest(r *http.Request) ( req *ReplyInput, rawBody []byte, @@ -409,6 +563,96 @@ func (s *Server) decodeSendEmailRequest(r *http.Request) ( } } +func (s *Server) decodeStartCliLoginRequest(r *http.Request) ( + req OptStartCliLoginInput, + rawBody []byte, + close func() error, + rerr error, +) { + var closers []func() error + close = func() error { + var merr error + // Close in reverse order, to match defer behavior. + for i := len(closers) - 1; i >= 0; i-- { + c := closers[i] + merr = errors.Join(merr, c()) + } + return merr + } + defer func() { + if rerr != nil { + rerr = errors.Join(rerr, close()) + } + }() + if _, ok := r.Header["Content-Type"]; !ok && r.ContentLength == 0 { + return req, rawBody, close, nil + } + ct, _, err := mime.ParseMediaType(r.Header.Get("Content-Type")) + if err != nil { + return req, rawBody, close, errors.Wrap(err, "parse media type") + } + switch { + case ct == "application/json": + if r.ContentLength == 0 { + return req, rawBody, close, nil + } + buf, err := io.ReadAll(r.Body) + defer func() { + _ = r.Body.Close() + }() + if err != nil { + return req, rawBody, close, err + } + + // Reset the body to allow for downstream reading. + r.Body = io.NopCloser(bytes.NewBuffer(buf)) + + if len(buf) == 0 { + return req, rawBody, close, nil + } + + rawBody = append(rawBody, buf...) + d := jx.DecodeBytes(buf) + + var request OptStartCliLoginInput + if err := func() error { + request.Reset() + if err := request.Decode(d); err != nil { + return err + } + if err := d.Skip(); err != io.EOF { + return errors.New("unexpected trailing data") + } + return nil + }(); err != nil { + err = &ogenerrors.DecodeBodyError{ + ContentType: ct, + Body: buf, + Err: err, + } + return req, rawBody, close, err + } + if err := func() error { + if value, ok := request.Get(); ok { + if err := func() error { + if err := value.Validate(); err != nil { + return err + } + return nil + }(); err != nil { + return err + } + } + return nil + }(); err != nil { + return req, rawBody, close, errors.Wrap(err, "validate") + } + return request, rawBody, close, nil + default: + return req, rawBody, close, validate.InvalidContentType(ct) + } +} + func (s *Server) decodeUpdateAccountRequest(r *http.Request) ( req *UpdateAccountInput, rawBody []byte, diff --git a/sdk-go/api/oas_request_encoders_gen.go b/sdk-go/api/oas_request_encoders_gen.go index 163f175..1579338 100644 --- a/sdk-go/api/oas_request_encoders_gen.go +++ b/sdk-go/api/oas_request_encoders_gen.go @@ -24,6 +24,26 @@ func encodeAddDomainRequest( return nil } +func encodeCliLogoutRequest( + req OptCliLogoutInput, + r *http.Request, +) error { + const contentType = "application/json" + if !req.Set { + // Keep request with empty body if value is not set. + return nil + } + e := new(jx.Encoder) + { + if req.Set { + req.Encode(e) + } + } + encoded := e.Bytes() + ht.SetBody(r, bytes.NewReader(encoded), contentType) + return nil +} + func encodeCreateEndpointRequest( req *CreateEndpointInput, r *http.Request, @@ -52,6 +72,20 @@ func encodeCreateFilterRequest( return nil } +func encodePollCliLoginRequest( + req *PollCliLoginInput, + r *http.Request, +) error { + const contentType = "application/json" + e := new(jx.Encoder) + { + req.Encode(e) + } + encoded := e.Bytes() + ht.SetBody(r, bytes.NewReader(encoded), contentType) + return nil +} + func encodeReplyToEmailRequest( req *ReplyInput, r *http.Request, @@ -80,6 +114,26 @@ func encodeSendEmailRequest( return nil } +func encodeStartCliLoginRequest( + req OptStartCliLoginInput, + r *http.Request, +) error { + const contentType = "application/json" + if !req.Set { + // Keep request with empty body if value is not set. + return nil + } + e := new(jx.Encoder) + { + if req.Set { + req.Encode(e) + } + } + encoded := e.Bytes() + ht.SetBody(r, bytes.NewReader(encoded), contentType) + return nil +} + func encodeUpdateAccountRequest( req *UpdateAccountInput, r *http.Request, diff --git a/sdk-go/api/oas_response_decoders_gen.go b/sdk-go/api/oas_response_decoders_gen.go index 0ec7073..4c3b950 100644 --- a/sdk-go/api/oas_response_decoders_gen.go +++ b/sdk-go/api/oas_response_decoders_gen.go @@ -189,6 +189,223 @@ func decodeAddDomainResponse(resp *http.Response) (res AddDomainRes, _ error) { return res, validate.UnexpectedStatusCodeWithResponse(resp) } +func decodeCliLogoutResponse(resp *http.Response) (res CliLogoutRes, _ error) { + switch resp.StatusCode { + case 200: + // Code 200. + ct, _, err := mime.ParseMediaType(resp.Header.Get("Content-Type")) + if err != nil { + return res, errors.Wrap(err, "parse media type") + } + switch { + case ct == "application/json": + buf, err := io.ReadAll(resp.Body) + if err != nil { + return res, err + } + d := jx.DecodeBytes(buf) + + var response CliLogoutOK + if err := func() error { + if err := response.Decode(d); err != nil { + return err + } + if err := d.Skip(); err != io.EOF { + return errors.New("unexpected trailing data") + } + return nil + }(); err != nil { + err = &ogenerrors.DecodeBodyError{ + ContentType: ct, + Body: buf, + Err: err, + } + return res, err + } + return &response, nil + default: + return res, validate.InvalidContentType(ct) + } + case 400: + // Code 400. + ct, _, err := mime.ParseMediaType(resp.Header.Get("Content-Type")) + if err != nil { + return res, errors.Wrap(err, "parse media type") + } + switch { + case ct == "application/json": + buf, err := io.ReadAll(resp.Body) + if err != nil { + return res, err + } + d := jx.DecodeBytes(buf) + + var response CliLogoutBadRequest + if err := func() error { + if err := response.Decode(d); err != nil { + return err + } + if err := d.Skip(); err != io.EOF { + return errors.New("unexpected trailing data") + } + return nil + }(); err != nil { + err = &ogenerrors.DecodeBodyError{ + ContentType: ct, + Body: buf, + Err: err, + } + return res, err + } + // Validate response. + if err := func() error { + if err := response.Validate(); err != nil { + return err + } + return nil + }(); err != nil { + return res, errors.Wrap(err, "validate") + } + return &response, nil + default: + return res, validate.InvalidContentType(ct) + } + case 401: + // Code 401. + ct, _, err := mime.ParseMediaType(resp.Header.Get("Content-Type")) + if err != nil { + return res, errors.Wrap(err, "parse media type") + } + switch { + case ct == "application/json": + buf, err := io.ReadAll(resp.Body) + if err != nil { + return res, err + } + d := jx.DecodeBytes(buf) + + var response CliLogoutUnauthorized + if err := func() error { + if err := response.Decode(d); err != nil { + return err + } + if err := d.Skip(); err != io.EOF { + return errors.New("unexpected trailing data") + } + return nil + }(); err != nil { + err = &ogenerrors.DecodeBodyError{ + ContentType: ct, + Body: buf, + Err: err, + } + return res, err + } + // Validate response. + if err := func() error { + if err := response.Validate(); err != nil { + return err + } + return nil + }(); err != nil { + return res, errors.Wrap(err, "validate") + } + return &response, nil + default: + return res, validate.InvalidContentType(ct) + } + case 403: + // Code 403. + ct, _, err := mime.ParseMediaType(resp.Header.Get("Content-Type")) + if err != nil { + return res, errors.Wrap(err, "parse media type") + } + switch { + case ct == "application/json": + buf, err := io.ReadAll(resp.Body) + if err != nil { + return res, err + } + d := jx.DecodeBytes(buf) + + var response CliLogoutForbidden + if err := func() error { + if err := response.Decode(d); err != nil { + return err + } + if err := d.Skip(); err != io.EOF { + return errors.New("unexpected trailing data") + } + return nil + }(); err != nil { + err = &ogenerrors.DecodeBodyError{ + ContentType: ct, + Body: buf, + Err: err, + } + return res, err + } + // Validate response. + if err := func() error { + if err := response.Validate(); err != nil { + return err + } + return nil + }(); err != nil { + return res, errors.Wrap(err, "validate") + } + return &response, nil + default: + return res, validate.InvalidContentType(ct) + } + case 404: + // Code 404. + ct, _, err := mime.ParseMediaType(resp.Header.Get("Content-Type")) + if err != nil { + return res, errors.Wrap(err, "parse media type") + } + switch { + case ct == "application/json": + buf, err := io.ReadAll(resp.Body) + if err != nil { + return res, err + } + d := jx.DecodeBytes(buf) + + var response CliLogoutNotFound + if err := func() error { + if err := response.Decode(d); err != nil { + return err + } + if err := d.Skip(); err != io.EOF { + return errors.New("unexpected trailing data") + } + return nil + }(); err != nil { + err = &ogenerrors.DecodeBodyError{ + ContentType: ct, + Body: buf, + Err: err, + } + return res, err + } + // Validate response. + if err := func() error { + if err := response.Validate(); err != nil { + return err + } + return nil + }(); err != nil { + return res, errors.Wrap(err, "validate") + } + return &response, nil + default: + return res, validate.InvalidContentType(ct) + } + } + return res, validate.UnexpectedStatusCodeWithResponse(resp) +} + func decodeCreateEndpointResponse(resp *http.Response) (res CreateEndpointRes, _ error) { switch resp.StatusCode { case 201: @@ -3601,7 +3818,7 @@ func decodeListSentEmailsResponse(resp *http.Response) (res ListSentEmailsRes, _ return res, validate.UnexpectedStatusCodeWithResponse(resp) } -func decodeReplayDeliveryResponse(resp *http.Response) (res ReplayDeliveryRes, _ error) { +func decodePollCliLoginResponse(resp *http.Response) (res PollCliLoginRes, _ error) { switch resp.StatusCode { case 200: // Code 200. @@ -3617,7 +3834,7 @@ func decodeReplayDeliveryResponse(resp *http.Response) (res ReplayDeliveryRes, _ } d := jx.DecodeBytes(buf) - var response ReplayDeliveryOK + var response PollCliLoginOK if err := func() error { if err := response.Decode(d); err != nil { return err @@ -3634,7 +3851,47 @@ func decodeReplayDeliveryResponse(resp *http.Response) (res ReplayDeliveryRes, _ } return res, err } - return &response, nil + var wrapper PollCliLoginOKHeaders + wrapper.Response = response + h := uri.NewHeaderDecoder(resp.Header) + // Parse "Cache-Control" header. + { + cfg := uri.HeaderParameterDecodingConfig{ + Name: "Cache-Control", + Explode: false, + } + if err := func() error { + if err := h.HasParam(cfg); err == nil { + if err := h.DecodeParam(cfg, func(d uri.Decoder) error { + var wrapperDotCacheControlVal string + if err := func() error { + val, err := d.DecodeValue() + if err != nil { + return err + } + + c, err := conv.ToString(val) + if err != nil { + return err + } + + wrapperDotCacheControlVal = c + return nil + }(); err != nil { + return err + } + wrapper.CacheControl.SetTo(wrapperDotCacheControlVal) + return nil + }); err != nil { + return err + } + } + return nil + }(); err != nil { + return res, errors.Wrap(err, "parse Cache-Control header") + } + } + return &wrapper, nil default: return res, validate.InvalidContentType(ct) } @@ -3652,7 +3909,7 @@ func decodeReplayDeliveryResponse(resp *http.Response) (res ReplayDeliveryRes, _ } d := jx.DecodeBytes(buf) - var response ReplayDeliveryBadRequest + var response PollCliLoginBadRequest if err := func() error { if err := response.Decode(d); err != nil { return err @@ -3682,8 +3939,8 @@ func decodeReplayDeliveryResponse(resp *http.Response) (res ReplayDeliveryRes, _ default: return res, validate.InvalidContentType(ct) } - case 401: - // Code 401. + case 403: + // Code 403. ct, _, err := mime.ParseMediaType(resp.Header.Get("Content-Type")) if err != nil { return res, errors.Wrap(err, "parse media type") @@ -3696,7 +3953,7 @@ func decodeReplayDeliveryResponse(resp *http.Response) (res ReplayDeliveryRes, _ } d := jx.DecodeBytes(buf) - var response ReplayDeliveryUnauthorized + var response PollCliLoginForbidden if err := func() error { if err := response.Decode(d); err != nil { return err @@ -3726,8 +3983,221 @@ func decodeReplayDeliveryResponse(resp *http.Response) (res ReplayDeliveryRes, _ default: return res, validate.InvalidContentType(ct) } - case 404: - // Code 404. + case 429: + // Code 429. + ct, _, err := mime.ParseMediaType(resp.Header.Get("Content-Type")) + if err != nil { + return res, errors.Wrap(err, "parse media type") + } + switch { + case ct == "application/json": + buf, err := io.ReadAll(resp.Body) + if err != nil { + return res, err + } + d := jx.DecodeBytes(buf) + + var response ErrorResponse + if err := func() error { + if err := response.Decode(d); err != nil { + return err + } + if err := d.Skip(); err != io.EOF { + return errors.New("unexpected trailing data") + } + return nil + }(); err != nil { + err = &ogenerrors.DecodeBodyError{ + ContentType: ct, + Body: buf, + Err: err, + } + return res, err + } + // Validate response. + if err := func() error { + if err := response.Validate(); err != nil { + return err + } + return nil + }(); err != nil { + return res, errors.Wrap(err, "validate") + } + var wrapper ErrorResponseHeaders + wrapper.Response = response + h := uri.NewHeaderDecoder(resp.Header) + // Parse "Retry-After" header. + { + cfg := uri.HeaderParameterDecodingConfig{ + Name: "Retry-After", + Explode: false, + } + if err := func() error { + if err := h.HasParam(cfg); err == nil { + if err := h.DecodeParam(cfg, func(d uri.Decoder) error { + var wrapperDotRetryAfterVal int + if err := func() error { + val, err := d.DecodeValue() + if err != nil { + return err + } + + c, err := conv.ToInt(val) + if err != nil { + return err + } + + wrapperDotRetryAfterVal = c + return nil + }(); err != nil { + return err + } + wrapper.RetryAfter.SetTo(wrapperDotRetryAfterVal) + return nil + }); err != nil { + return err + } + } + return nil + }(); err != nil { + return res, errors.Wrap(err, "parse Retry-After header") + } + } + return &wrapper, nil + default: + return res, validate.InvalidContentType(ct) + } + } + return res, validate.UnexpectedStatusCodeWithResponse(resp) +} + +func decodeReplayDeliveryResponse(resp *http.Response) (res ReplayDeliveryRes, _ error) { + switch resp.StatusCode { + case 200: + // Code 200. + ct, _, err := mime.ParseMediaType(resp.Header.Get("Content-Type")) + if err != nil { + return res, errors.Wrap(err, "parse media type") + } + switch { + case ct == "application/json": + buf, err := io.ReadAll(resp.Body) + if err != nil { + return res, err + } + d := jx.DecodeBytes(buf) + + var response ReplayDeliveryOK + if err := func() error { + if err := response.Decode(d); err != nil { + return err + } + if err := d.Skip(); err != io.EOF { + return errors.New("unexpected trailing data") + } + return nil + }(); err != nil { + err = &ogenerrors.DecodeBodyError{ + ContentType: ct, + Body: buf, + Err: err, + } + return res, err + } + return &response, nil + default: + return res, validate.InvalidContentType(ct) + } + case 400: + // Code 400. + ct, _, err := mime.ParseMediaType(resp.Header.Get("Content-Type")) + if err != nil { + return res, errors.Wrap(err, "parse media type") + } + switch { + case ct == "application/json": + buf, err := io.ReadAll(resp.Body) + if err != nil { + return res, err + } + d := jx.DecodeBytes(buf) + + var response ReplayDeliveryBadRequest + if err := func() error { + if err := response.Decode(d); err != nil { + return err + } + if err := d.Skip(); err != io.EOF { + return errors.New("unexpected trailing data") + } + return nil + }(); err != nil { + err = &ogenerrors.DecodeBodyError{ + ContentType: ct, + Body: buf, + Err: err, + } + return res, err + } + // Validate response. + if err := func() error { + if err := response.Validate(); err != nil { + return err + } + return nil + }(); err != nil { + return res, errors.Wrap(err, "validate") + } + return &response, nil + default: + return res, validate.InvalidContentType(ct) + } + case 401: + // Code 401. + ct, _, err := mime.ParseMediaType(resp.Header.Get("Content-Type")) + if err != nil { + return res, errors.Wrap(err, "parse media type") + } + switch { + case ct == "application/json": + buf, err := io.ReadAll(resp.Body) + if err != nil { + return res, err + } + d := jx.DecodeBytes(buf) + + var response ReplayDeliveryUnauthorized + if err := func() error { + if err := response.Decode(d); err != nil { + return err + } + if err := d.Skip(); err != io.EOF { + return errors.New("unexpected trailing data") + } + return nil + }(); err != nil { + err = &ogenerrors.DecodeBodyError{ + ContentType: ct, + Body: buf, + Err: err, + } + return res, err + } + // Validate response. + if err := func() error { + if err := response.Validate(); err != nil { + return err + } + return nil + }(); err != nil { + return res, errors.Wrap(err, "validate") + } + return &response, nil + default: + return res, validate.InvalidContentType(ct) + } + case 404: + // Code 404. ct, _, err := mime.ParseMediaType(resp.Header.Get("Content-Type")) if err != nil { return res, errors.Wrap(err, "parse media type") @@ -5256,6 +5726,224 @@ func decodeSendEmailResponse(resp *http.Response) (res SendEmailRes, _ error) { return res, validate.UnexpectedStatusCodeWithResponse(resp) } +func decodeStartCliLoginResponse(resp *http.Response) (res StartCliLoginRes, _ error) { + switch resp.StatusCode { + case 201: + // Code 201. + ct, _, err := mime.ParseMediaType(resp.Header.Get("Content-Type")) + if err != nil { + return res, errors.Wrap(err, "parse media type") + } + switch { + case ct == "application/json": + buf, err := io.ReadAll(resp.Body) + if err != nil { + return res, err + } + d := jx.DecodeBytes(buf) + + var response StartCliLoginCreated + if err := func() error { + if err := response.Decode(d); err != nil { + return err + } + if err := d.Skip(); err != io.EOF { + return errors.New("unexpected trailing data") + } + return nil + }(); err != nil { + err = &ogenerrors.DecodeBodyError{ + ContentType: ct, + Body: buf, + Err: err, + } + return res, err + } + // Validate response. + if err := func() error { + if err := response.Validate(); err != nil { + return err + } + return nil + }(); err != nil { + return res, errors.Wrap(err, "validate") + } + var wrapper StartCliLoginCreatedHeaders + wrapper.Response = response + h := uri.NewHeaderDecoder(resp.Header) + // Parse "Cache-Control" header. + { + cfg := uri.HeaderParameterDecodingConfig{ + Name: "Cache-Control", + Explode: false, + } + if err := func() error { + if err := h.HasParam(cfg); err == nil { + if err := h.DecodeParam(cfg, func(d uri.Decoder) error { + var wrapperDotCacheControlVal string + if err := func() error { + val, err := d.DecodeValue() + if err != nil { + return err + } + + c, err := conv.ToString(val) + if err != nil { + return err + } + + wrapperDotCacheControlVal = c + return nil + }(); err != nil { + return err + } + wrapper.CacheControl.SetTo(wrapperDotCacheControlVal) + return nil + }); err != nil { + return err + } + } + return nil + }(); err != nil { + return res, errors.Wrap(err, "parse Cache-Control header") + } + } + return &wrapper, nil + default: + return res, validate.InvalidContentType(ct) + } + case 400: + // Code 400. + ct, _, err := mime.ParseMediaType(resp.Header.Get("Content-Type")) + if err != nil { + return res, errors.Wrap(err, "parse media type") + } + switch { + case ct == "application/json": + buf, err := io.ReadAll(resp.Body) + if err != nil { + return res, err + } + d := jx.DecodeBytes(buf) + + var response ErrorResponse + if err := func() error { + if err := response.Decode(d); err != nil { + return err + } + if err := d.Skip(); err != io.EOF { + return errors.New("unexpected trailing data") + } + return nil + }(); err != nil { + err = &ogenerrors.DecodeBodyError{ + ContentType: ct, + Body: buf, + Err: err, + } + return res, err + } + // Validate response. + if err := func() error { + if err := response.Validate(); err != nil { + return err + } + return nil + }(); err != nil { + return res, errors.Wrap(err, "validate") + } + return &response, nil + default: + return res, validate.InvalidContentType(ct) + } + case 429: + // Code 429. + ct, _, err := mime.ParseMediaType(resp.Header.Get("Content-Type")) + if err != nil { + return res, errors.Wrap(err, "parse media type") + } + switch { + case ct == "application/json": + buf, err := io.ReadAll(resp.Body) + if err != nil { + return res, err + } + d := jx.DecodeBytes(buf) + + var response ErrorResponse + if err := func() error { + if err := response.Decode(d); err != nil { + return err + } + if err := d.Skip(); err != io.EOF { + return errors.New("unexpected trailing data") + } + return nil + }(); err != nil { + err = &ogenerrors.DecodeBodyError{ + ContentType: ct, + Body: buf, + Err: err, + } + return res, err + } + // Validate response. + if err := func() error { + if err := response.Validate(); err != nil { + return err + } + return nil + }(); err != nil { + return res, errors.Wrap(err, "validate") + } + var wrapper RateLimitedHeaders + wrapper.Response = response + h := uri.NewHeaderDecoder(resp.Header) + // Parse "Retry-After" header. + { + cfg := uri.HeaderParameterDecodingConfig{ + Name: "Retry-After", + Explode: false, + } + if err := func() error { + if err := h.HasParam(cfg); err == nil { + if err := h.DecodeParam(cfg, func(d uri.Decoder) error { + var wrapperDotRetryAfterVal int + if err := func() error { + val, err := d.DecodeValue() + if err != nil { + return err + } + + c, err := conv.ToInt(val) + if err != nil { + return err + } + + wrapperDotRetryAfterVal = c + return nil + }(); err != nil { + return err + } + wrapper.RetryAfter.SetTo(wrapperDotRetryAfterVal) + return nil + }); err != nil { + return err + } + } + return nil + }(); err != nil { + return res, errors.Wrap(err, "parse Retry-After header") + } + } + return &wrapper, nil + default: + return res, validate.InvalidContentType(ct) + } + } + return res, validate.UnexpectedStatusCodeWithResponse(resp) +} + func decodeTestEndpointResponse(resp *http.Response) (res TestEndpointRes, _ error) { switch resp.StatusCode { case 200: diff --git a/sdk-go/api/oas_response_encoders_gen.go b/sdk-go/api/oas_response_encoders_gen.go index 2ce92a1..ca7d459 100644 --- a/sdk-go/api/oas_response_encoders_gen.go +++ b/sdk-go/api/oas_response_encoders_gen.go @@ -73,6 +73,78 @@ func encodeAddDomainResponse(response AddDomainRes, w http.ResponseWriter, span } } +func encodeCliLogoutResponse(response CliLogoutRes, w http.ResponseWriter, span trace.Span) error { + switch response := response.(type) { + case *CliLogoutOK: + w.Header().Set("Content-Type", "application/json; charset=utf-8") + w.WriteHeader(200) + span.SetStatus(codes.Ok, http.StatusText(200)) + + e := new(jx.Encoder) + response.Encode(e) + if _, err := e.WriteTo(w); err != nil { + return errors.Wrap(err, "write") + } + + return nil + + case *CliLogoutBadRequest: + w.Header().Set("Content-Type", "application/json; charset=utf-8") + w.WriteHeader(400) + span.SetStatus(codes.Error, http.StatusText(400)) + + e := new(jx.Encoder) + response.Encode(e) + if _, err := e.WriteTo(w); err != nil { + return errors.Wrap(err, "write") + } + + return nil + + case *CliLogoutUnauthorized: + w.Header().Set("Content-Type", "application/json; charset=utf-8") + w.WriteHeader(401) + span.SetStatus(codes.Error, http.StatusText(401)) + + e := new(jx.Encoder) + response.Encode(e) + if _, err := e.WriteTo(w); err != nil { + return errors.Wrap(err, "write") + } + + return nil + + case *CliLogoutForbidden: + w.Header().Set("Content-Type", "application/json; charset=utf-8") + w.WriteHeader(403) + span.SetStatus(codes.Error, http.StatusText(403)) + + e := new(jx.Encoder) + response.Encode(e) + if _, err := e.WriteTo(w); err != nil { + return errors.Wrap(err, "write") + } + + return nil + + case *CliLogoutNotFound: + w.Header().Set("Content-Type", "application/json; charset=utf-8") + w.WriteHeader(404) + span.SetStatus(codes.Error, http.StatusText(404)) + + e := new(jx.Encoder) + response.Encode(e) + if _, err := e.WriteTo(w); err != nil { + return errors.Wrap(err, "write") + } + + return nil + + default: + return errors.Errorf("unexpected response type: %T", response) + } +} + func encodeCreateEndpointResponse(response CreateEndpointRes, w http.ResponseWriter, span trace.Span) error { switch response := response.(type) { case *CreateEndpointCreated: @@ -1265,6 +1337,104 @@ func encodeListSentEmailsResponse(response ListSentEmailsRes, w http.ResponseWri } } +func encodePollCliLoginResponse(response PollCliLoginRes, w http.ResponseWriter, span trace.Span) error { + switch response := response.(type) { + case *PollCliLoginOKHeaders: + w.Header().Set("Content-Type", "application/json; charset=utf-8") + // Encoding response headers. + { + h := uri.NewHeaderEncoder(w.Header()) + // Encode "Cache-Control" header. + { + cfg := uri.HeaderParameterEncodingConfig{ + Name: "Cache-Control", + Explode: false, + } + if err := h.EncodeParam(cfg, func(e uri.Encoder) error { + if val, ok := response.CacheControl.Get(); ok { + return e.EncodeValue(conv.StringToString(val)) + } + return nil + }); err != nil { + return errors.Wrap(err, "encode Cache-Control header") + } + } + } + w.WriteHeader(200) + span.SetStatus(codes.Ok, http.StatusText(200)) + + e := new(jx.Encoder) + response.Response.Encode(e) + if _, err := e.WriteTo(w); err != nil { + return errors.Wrap(err, "write") + } + + return nil + + case *PollCliLoginBadRequest: + w.Header().Set("Content-Type", "application/json; charset=utf-8") + w.WriteHeader(400) + span.SetStatus(codes.Error, http.StatusText(400)) + + e := new(jx.Encoder) + response.Encode(e) + if _, err := e.WriteTo(w); err != nil { + return errors.Wrap(err, "write") + } + + return nil + + case *PollCliLoginForbidden: + w.Header().Set("Content-Type", "application/json; charset=utf-8") + w.WriteHeader(403) + span.SetStatus(codes.Error, http.StatusText(403)) + + e := new(jx.Encoder) + response.Encode(e) + if _, err := e.WriteTo(w); err != nil { + return errors.Wrap(err, "write") + } + + return nil + + case *ErrorResponseHeaders: + w.Header().Set("Content-Type", "application/json; charset=utf-8") + w.Header().Set("Access-Control-Expose-Headers", "Retry-After") + // Encoding response headers. + { + h := uri.NewHeaderEncoder(w.Header()) + // Encode "Retry-After" header. + { + cfg := uri.HeaderParameterEncodingConfig{ + Name: "Retry-After", + Explode: false, + } + if err := h.EncodeParam(cfg, func(e uri.Encoder) error { + if val, ok := response.RetryAfter.Get(); ok { + return e.EncodeValue(conv.IntToString(val)) + } + return nil + }); err != nil { + return errors.Wrap(err, "encode Retry-After header") + } + } + } + w.WriteHeader(429) + span.SetStatus(codes.Error, http.StatusText(429)) + + e := new(jx.Encoder) + response.Response.Encode(e) + if _, err := e.WriteTo(w); err != nil { + return errors.Wrap(err, "write") + } + + return nil + + default: + return errors.Errorf("unexpected response type: %T", response) + } +} + func encodeReplayDeliveryResponse(response ReplayDeliveryRes, w http.ResponseWriter, span trace.Span) error { switch response := response.(type) { case *ReplayDeliveryOK: @@ -1829,6 +1999,91 @@ func encodeSendEmailResponse(response SendEmailRes, w http.ResponseWriter, span } } +func encodeStartCliLoginResponse(response StartCliLoginRes, w http.ResponseWriter, span trace.Span) error { + switch response := response.(type) { + case *StartCliLoginCreatedHeaders: + w.Header().Set("Content-Type", "application/json; charset=utf-8") + // Encoding response headers. + { + h := uri.NewHeaderEncoder(w.Header()) + // Encode "Cache-Control" header. + { + cfg := uri.HeaderParameterEncodingConfig{ + Name: "Cache-Control", + Explode: false, + } + if err := h.EncodeParam(cfg, func(e uri.Encoder) error { + if val, ok := response.CacheControl.Get(); ok { + return e.EncodeValue(conv.StringToString(val)) + } + return nil + }); err != nil { + return errors.Wrap(err, "encode Cache-Control header") + } + } + } + w.WriteHeader(201) + span.SetStatus(codes.Ok, http.StatusText(201)) + + e := new(jx.Encoder) + response.Response.Encode(e) + if _, err := e.WriteTo(w); err != nil { + return errors.Wrap(err, "write") + } + + return nil + + case *ErrorResponse: + w.Header().Set("Content-Type", "application/json; charset=utf-8") + w.WriteHeader(400) + span.SetStatus(codes.Error, http.StatusText(400)) + + e := new(jx.Encoder) + response.Encode(e) + if _, err := e.WriteTo(w); err != nil { + return errors.Wrap(err, "write") + } + + return nil + + case *RateLimitedHeaders: + w.Header().Set("Content-Type", "application/json; charset=utf-8") + w.Header().Set("Access-Control-Expose-Headers", "Retry-After") + // Encoding response headers. + { + h := uri.NewHeaderEncoder(w.Header()) + // Encode "Retry-After" header. + { + cfg := uri.HeaderParameterEncodingConfig{ + Name: "Retry-After", + Explode: false, + } + if err := h.EncodeParam(cfg, func(e uri.Encoder) error { + if val, ok := response.RetryAfter.Get(); ok { + return e.EncodeValue(conv.IntToString(val)) + } + return nil + }); err != nil { + return errors.Wrap(err, "encode Retry-After header") + } + } + } + w.WriteHeader(429) + span.SetStatus(codes.Error, http.StatusText(429)) + + e := new(jx.Encoder) + response.Response.Encode(e) + if _, err := e.WriteTo(w); err != nil { + return errors.Wrap(err, "write") + } + + return nil + + default: + return errors.Errorf("unexpected response type: %T", response) + } +} + func encodeTestEndpointResponse(response TestEndpointRes, w http.ResponseWriter, span trace.Span) error { switch response := response.(type) { case *TestEndpointOK: diff --git a/sdk-go/api/oas_router_gen.go b/sdk-go/api/oas_router_gen.go index 082cca4..c4cf1b3 100644 --- a/sdk-go/api/oas_router_gen.go +++ b/sdk-go/api/oas_router_gen.go @@ -11,87 +11,96 @@ import ( ) var ( - rn18AllowedHeaders = map[string]string{ + rn19AllowedHeaders = map[string]string{ "GET": "Authorization", "PATCH": "Authorization,Content-Type", } - rn23AllowedHeaders = map[string]string{ + rn24AllowedHeaders = map[string]string{ "GET": "Authorization", } - rn25AllowedHeaders = map[string]string{ + rn26AllowedHeaders = map[string]string{ "GET": "Authorization", } - rn36AllowedHeaders = map[string]string{ + rn39AllowedHeaders = map[string]string{ "POST": "Authorization", } + rn31AllowedHeaders = map[string]string{ + "POST": "Content-Type", + } + rn43AllowedHeaders = map[string]string{ + "POST": "Content-Type", + } + rn3AllowedHeaders = map[string]string{ + "POST": "Authorization,Content-Type", + } rn1AllowedHeaders = map[string]string{ "GET": "Authorization", "POST": "Authorization,Content-Type", } - rn6AllowedHeaders = map[string]string{ + rn7AllowedHeaders = map[string]string{ "DELETE": "Authorization", "PATCH": "Authorization,Content-Type", } - rn40AllowedHeaders = map[string]string{ + rn45AllowedHeaders = map[string]string{ "POST": "Authorization", } - rn27AllowedHeaders = map[string]string{ + rn28AllowedHeaders = map[string]string{ "GET": "Authorization", } - rn9AllowedHeaders = map[string]string{ + rn10AllowedHeaders = map[string]string{ "DELETE": "Authorization", "GET": "Authorization", } - rn16AllowedHeaders = map[string]string{ + rn17AllowedHeaders = map[string]string{ "GET": "Authorization", } - rn14AllowedHeaders = map[string]string{ + rn15AllowedHeaders = map[string]string{ "POST": "Authorization", } - rn17AllowedHeaders = map[string]string{ + rn18AllowedHeaders = map[string]string{ "GET": "Authorization", } - rn33AllowedHeaders = map[string]string{ + rn36AllowedHeaders = map[string]string{ "POST": "Authorization", } - rn35AllowedHeaders = map[string]string{ + rn38AllowedHeaders = map[string]string{ "POST": "Authorization,Content-Type", } - rn3AllowedHeaders = map[string]string{ + rn4AllowedHeaders = map[string]string{ "GET": "Authorization", "POST": "Authorization,Content-Type", } - rn11AllowedHeaders = map[string]string{ + rn12AllowedHeaders = map[string]string{ "DELETE": "Authorization", "PATCH": "Authorization,Content-Type", } - rn39AllowedHeaders = map[string]string{ + rn44AllowedHeaders = map[string]string{ "POST": "Authorization", } - rn4AllowedHeaders = map[string]string{ + rn5AllowedHeaders = map[string]string{ "GET": "Authorization", "POST": "Authorization,Content-Type", } - rn13AllowedHeaders = map[string]string{ + rn14AllowedHeaders = map[string]string{ "DELETE": "Authorization", "PATCH": "Authorization,Content-Type", } - rn38AllowedHeaders = map[string]string{ + rn41AllowedHeaders = map[string]string{ "POST": "Authorization,Content-Type,Idempotency-Key", } - rn19AllowedHeaders = map[string]string{ + rn20AllowedHeaders = map[string]string{ "GET": "Authorization", } - rn28AllowedHeaders = map[string]string{ + rn29AllowedHeaders = map[string]string{ "GET": "Authorization", } - rn22AllowedHeaders = map[string]string{ + rn23AllowedHeaders = map[string]string{ "GET": "Authorization", } - rn26AllowedHeaders = map[string]string{ + rn27AllowedHeaders = map[string]string{ "GET": "Authorization", } - rn31AllowedHeaders = map[string]string{ + rn34AllowedHeaders = map[string]string{ "POST": "Authorization", } ) @@ -164,7 +173,7 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { default: s.notAllowed(w, r, notAllowedParams{ allowedMethods: "GET,PATCH", - allowedHeaders: rn18AllowedHeaders, + allowedHeaders: rn19AllowedHeaders, acceptPost: "", acceptPatch: "application/json", }) @@ -201,7 +210,7 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { default: s.notAllowed(w, r, notAllowedParams{ allowedMethods: "GET", - allowedHeaders: rn23AllowedHeaders, + allowedHeaders: rn24AllowedHeaders, acceptPost: "", acceptPatch: "", }) @@ -225,7 +234,7 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { default: s.notAllowed(w, r, notAllowedParams{ allowedMethods: "GET", - allowedHeaders: rn25AllowedHeaders, + allowedHeaders: rn26AllowedHeaders, acceptPost: "", acceptPatch: "", }) @@ -250,7 +259,7 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { default: s.notAllowed(w, r, notAllowedParams{ allowedMethods: "POST", - allowedHeaders: rn36AllowedHeaders, + allowedHeaders: rn39AllowedHeaders, acceptPost: "", acceptPatch: "", }) @@ -265,6 +274,109 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { } + case 'c': // Prefix: "cli/log" + + if l := len("cli/log"); len(elem) >= l && elem[0:l] == "cli/log" { + elem = elem[l:] + } else { + break + } + + if len(elem) == 0 { + break + } + switch elem[0] { + case 'i': // Prefix: "in/" + + if l := len("in/"); len(elem) >= l && elem[0:l] == "in/" { + elem = elem[l:] + } else { + break + } + + if len(elem) == 0 { + break + } + switch elem[0] { + case 'p': // Prefix: "poll" + + if l := len("poll"); len(elem) >= l && elem[0:l] == "poll" { + elem = elem[l:] + } else { + break + } + + if len(elem) == 0 { + // Leaf node. + switch r.Method { + case "POST": + s.handlePollCliLoginRequest([0]string{}, elemIsEscaped, w, r) + default: + s.notAllowed(w, r, notAllowedParams{ + allowedMethods: "POST", + allowedHeaders: rn31AllowedHeaders, + acceptPost: "application/json", + acceptPatch: "", + }) + } + + return + } + + case 's': // Prefix: "start" + + if l := len("start"); len(elem) >= l && elem[0:l] == "start" { + elem = elem[l:] + } else { + break + } + + if len(elem) == 0 { + // Leaf node. + switch r.Method { + case "POST": + s.handleStartCliLoginRequest([0]string{}, elemIsEscaped, w, r) + default: + s.notAllowed(w, r, notAllowedParams{ + allowedMethods: "POST", + allowedHeaders: rn43AllowedHeaders, + acceptPost: "application/json", + acceptPatch: "", + }) + } + + return + } + + } + + case 'o': // Prefix: "out" + + if l := len("out"); len(elem) >= l && elem[0:l] == "out" { + elem = elem[l:] + } else { + break + } + + if len(elem) == 0 { + // Leaf node. + switch r.Method { + case "POST": + s.handleCliLogoutRequest([0]string{}, elemIsEscaped, w, r) + default: + s.notAllowed(w, r, notAllowedParams{ + allowedMethods: "POST", + allowedHeaders: rn3AllowedHeaders, + acceptPost: "application/json", + acceptPatch: "", + }) + } + + return + } + + } + case 'd': // Prefix: "domains" if l := len("domains"); len(elem) >= l && elem[0:l] == "domains" { @@ -321,7 +433,7 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { default: s.notAllowed(w, r, notAllowedParams{ allowedMethods: "DELETE,PATCH", - allowedHeaders: rn6AllowedHeaders, + allowedHeaders: rn7AllowedHeaders, acceptPost: "", acceptPatch: "application/json", }) @@ -348,7 +460,7 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { default: s.notAllowed(w, r, notAllowedParams{ allowedMethods: "POST", - allowedHeaders: rn40AllowedHeaders, + allowedHeaders: rn45AllowedHeaders, acceptPost: "", acceptPatch: "", }) @@ -388,7 +500,7 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { default: s.notAllowed(w, r, notAllowedParams{ allowedMethods: "GET", - allowedHeaders: rn27AllowedHeaders, + allowedHeaders: rn28AllowedHeaders, acceptPost: "", acceptPatch: "", }) @@ -427,7 +539,7 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { default: s.notAllowed(w, r, notAllowedParams{ allowedMethods: "DELETE,GET", - allowedHeaders: rn9AllowedHeaders, + allowedHeaders: rn10AllowedHeaders, acceptPost: "", acceptPatch: "", }) @@ -466,7 +578,7 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { default: s.notAllowed(w, r, notAllowedParams{ allowedMethods: "GET", - allowedHeaders: rn16AllowedHeaders, + allowedHeaders: rn17AllowedHeaders, acceptPost: "", acceptPatch: "", }) @@ -493,7 +605,7 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { default: s.notAllowed(w, r, notAllowedParams{ allowedMethods: "POST", - allowedHeaders: rn14AllowedHeaders, + allowedHeaders: rn15AllowedHeaders, acceptPost: "", acceptPatch: "", }) @@ -532,7 +644,7 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { default: s.notAllowed(w, r, notAllowedParams{ allowedMethods: "GET", - allowedHeaders: rn17AllowedHeaders, + allowedHeaders: rn18AllowedHeaders, acceptPost: "", acceptPatch: "", }) @@ -571,7 +683,7 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { default: s.notAllowed(w, r, notAllowedParams{ allowedMethods: "POST", - allowedHeaders: rn33AllowedHeaders, + allowedHeaders: rn36AllowedHeaders, acceptPost: "", acceptPatch: "", }) @@ -598,7 +710,7 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { default: s.notAllowed(w, r, notAllowedParams{ allowedMethods: "POST", - allowedHeaders: rn35AllowedHeaders, + allowedHeaders: rn38AllowedHeaders, acceptPost: "application/json", acceptPatch: "", }) @@ -634,7 +746,7 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { default: s.notAllowed(w, r, notAllowedParams{ allowedMethods: "GET,POST", - allowedHeaders: rn3AllowedHeaders, + allowedHeaders: rn4AllowedHeaders, acceptPost: "application/json", acceptPatch: "", }) @@ -673,7 +785,7 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { default: s.notAllowed(w, r, notAllowedParams{ allowedMethods: "DELETE,PATCH", - allowedHeaders: rn11AllowedHeaders, + allowedHeaders: rn12AllowedHeaders, acceptPost: "", acceptPatch: "application/json", }) @@ -700,7 +812,7 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { default: s.notAllowed(w, r, notAllowedParams{ allowedMethods: "POST", - allowedHeaders: rn39AllowedHeaders, + allowedHeaders: rn44AllowedHeaders, acceptPost: "", acceptPatch: "", }) @@ -732,7 +844,7 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { default: s.notAllowed(w, r, notAllowedParams{ allowedMethods: "GET,POST", - allowedHeaders: rn4AllowedHeaders, + allowedHeaders: rn5AllowedHeaders, acceptPost: "application/json", acceptPatch: "", }) @@ -772,7 +884,7 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { default: s.notAllowed(w, r, notAllowedParams{ allowedMethods: "DELETE,PATCH", - allowedHeaders: rn13AllowedHeaders, + allowedHeaders: rn14AllowedHeaders, acceptPost: "", acceptPatch: "application/json", }) @@ -823,7 +935,7 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { default: s.notAllowed(w, r, notAllowedParams{ allowedMethods: "POST", - allowedHeaders: rn38AllowedHeaders, + allowedHeaders: rn41AllowedHeaders, acceptPost: "application/json", acceptPatch: "", }) @@ -848,7 +960,7 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { default: s.notAllowed(w, r, notAllowedParams{ allowedMethods: "GET", - allowedHeaders: rn19AllowedHeaders, + allowedHeaders: rn20AllowedHeaders, acceptPost: "", acceptPatch: "", }) @@ -874,7 +986,7 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { default: s.notAllowed(w, r, notAllowedParams{ allowedMethods: "GET", - allowedHeaders: rn28AllowedHeaders, + allowedHeaders: rn29AllowedHeaders, acceptPost: "", acceptPatch: "", }) @@ -910,7 +1022,7 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { default: s.notAllowed(w, r, notAllowedParams{ allowedMethods: "GET", - allowedHeaders: rn22AllowedHeaders, + allowedHeaders: rn23AllowedHeaders, acceptPost: "", acceptPatch: "", }) @@ -938,7 +1050,7 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { default: s.notAllowed(w, r, notAllowedParams{ allowedMethods: "GET", - allowedHeaders: rn26AllowedHeaders, + allowedHeaders: rn27AllowedHeaders, acceptPost: "", acceptPatch: "", }) @@ -986,7 +1098,7 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { default: s.notAllowed(w, r, notAllowedParams{ allowedMethods: "POST", - allowedHeaders: rn31AllowedHeaders, + allowedHeaders: rn34AllowedHeaders, acceptPost: "", acceptPatch: "", }) @@ -1224,6 +1336,109 @@ func (s *Server) FindPath(method string, u *url.URL) (r Route, _ bool) { } + case 'c': // Prefix: "cli/log" + + if l := len("cli/log"); len(elem) >= l && elem[0:l] == "cli/log" { + elem = elem[l:] + } else { + break + } + + if len(elem) == 0 { + break + } + switch elem[0] { + case 'i': // Prefix: "in/" + + if l := len("in/"); len(elem) >= l && elem[0:l] == "in/" { + elem = elem[l:] + } else { + break + } + + if len(elem) == 0 { + break + } + switch elem[0] { + case 'p': // Prefix: "poll" + + if l := len("poll"); len(elem) >= l && elem[0:l] == "poll" { + elem = elem[l:] + } else { + break + } + + if len(elem) == 0 { + // Leaf node. + switch method { + case "POST": + r.name = PollCliLoginOperation + r.summary = "Poll CLI browser login" + r.operationID = "pollCliLogin" + r.operationGroup = "" + r.pathPattern = "/cli/login/poll" + r.args = args + r.count = 0 + return r, true + default: + return + } + } + + case 's': // Prefix: "start" + + if l := len("start"); len(elem) >= l && elem[0:l] == "start" { + elem = elem[l:] + } else { + break + } + + if len(elem) == 0 { + // Leaf node. + switch method { + case "POST": + r.name = StartCliLoginOperation + r.summary = "Start CLI browser login" + r.operationID = "startCliLogin" + r.operationGroup = "" + r.pathPattern = "/cli/login/start" + r.args = args + r.count = 0 + return r, true + default: + return + } + } + + } + + case 'o': // Prefix: "out" + + if l := len("out"); len(elem) >= l && elem[0:l] == "out" { + elem = elem[l:] + } else { + break + } + + if len(elem) == 0 { + // Leaf node. + switch method { + case "POST": + r.name = CliLogoutOperation + r.summary = "Revoke the current CLI API key" + r.operationID = "cliLogout" + r.operationGroup = "" + r.pathPattern = "/cli/logout" + r.args = args + r.count = 0 + return r, true + default: + return + } + } + + } + case 'd': // Prefix: "domains" if l := len("domains"); len(elem) >= l && elem[0:l] == "domains" { diff --git a/sdk-go/api/oas_schemas_gen.go b/sdk-go/api/oas_schemas_gen.go index e524c2c..b56340b 100644 --- a/sdk-go/api/oas_schemas_gen.go +++ b/sdk-go/api/oas_schemas_gen.go @@ -4,6 +4,7 @@ package api import ( "io" + "net/url" "time" "github.com/go-faster/errors" @@ -287,6 +288,228 @@ func (s *BearerAuth) SetRoles(val []string) { s.Roles = val } +// Ref: #/components/schemas/CliLoginPollResult +type CliLoginPollResult struct { + // Newly-created API key for CLI authentication. + APIKey string `json:"api_key"` + KeyID uuid.UUID `json:"key_id"` + KeyPrefix string `json:"key_prefix"` + OrgID uuid.UUID `json:"org_id"` + OrgName NilString `json:"org_name"` +} + +// GetAPIKey returns the value of APIKey. +func (s *CliLoginPollResult) GetAPIKey() string { + return s.APIKey +} + +// GetKeyID returns the value of KeyID. +func (s *CliLoginPollResult) GetKeyID() uuid.UUID { + return s.KeyID +} + +// GetKeyPrefix returns the value of KeyPrefix. +func (s *CliLoginPollResult) GetKeyPrefix() string { + return s.KeyPrefix +} + +// GetOrgID returns the value of OrgID. +func (s *CliLoginPollResult) GetOrgID() uuid.UUID { + return s.OrgID +} + +// GetOrgName returns the value of OrgName. +func (s *CliLoginPollResult) GetOrgName() NilString { + return s.OrgName +} + +// SetAPIKey sets the value of APIKey. +func (s *CliLoginPollResult) SetAPIKey(val string) { + s.APIKey = val +} + +// SetKeyID sets the value of KeyID. +func (s *CliLoginPollResult) SetKeyID(val uuid.UUID) { + s.KeyID = val +} + +// SetKeyPrefix sets the value of KeyPrefix. +func (s *CliLoginPollResult) SetKeyPrefix(val string) { + s.KeyPrefix = val +} + +// SetOrgID sets the value of OrgID. +func (s *CliLoginPollResult) SetOrgID(val uuid.UUID) { + s.OrgID = val +} + +// SetOrgName sets the value of OrgName. +func (s *CliLoginPollResult) SetOrgName(val NilString) { + s.OrgName = val +} + +// Ref: #/components/schemas/CliLoginStartResult +type CliLoginStartResult struct { + // Opaque code used by the CLI to poll for approval. + DeviceCode string `json:"device_code"` + // Short code the user confirms in the browser. + UserCode string `json:"user_code"` + // Browser URL where the user approves the login. + VerificationURI url.URL `json:"verification_uri"` + // Browser URL with the user code prefilled. + VerificationURIComplete url.URL `json:"verification_uri_complete"` + // Seconds until the login session expires. + ExpiresIn int `json:"expires_in"` + // Minimum seconds between poll requests. + Interval int `json:"interval"` +} + +// GetDeviceCode returns the value of DeviceCode. +func (s *CliLoginStartResult) GetDeviceCode() string { + return s.DeviceCode +} + +// GetUserCode returns the value of UserCode. +func (s *CliLoginStartResult) GetUserCode() string { + return s.UserCode +} + +// GetVerificationURI returns the value of VerificationURI. +func (s *CliLoginStartResult) GetVerificationURI() url.URL { + return s.VerificationURI +} + +// GetVerificationURIComplete returns the value of VerificationURIComplete. +func (s *CliLoginStartResult) GetVerificationURIComplete() url.URL { + return s.VerificationURIComplete +} + +// GetExpiresIn returns the value of ExpiresIn. +func (s *CliLoginStartResult) GetExpiresIn() int { + return s.ExpiresIn +} + +// GetInterval returns the value of Interval. +func (s *CliLoginStartResult) GetInterval() int { + return s.Interval +} + +// SetDeviceCode sets the value of DeviceCode. +func (s *CliLoginStartResult) SetDeviceCode(val string) { + s.DeviceCode = val +} + +// SetUserCode sets the value of UserCode. +func (s *CliLoginStartResult) SetUserCode(val string) { + s.UserCode = val +} + +// SetVerificationURI sets the value of VerificationURI. +func (s *CliLoginStartResult) SetVerificationURI(val url.URL) { + s.VerificationURI = val +} + +// SetVerificationURIComplete sets the value of VerificationURIComplete. +func (s *CliLoginStartResult) SetVerificationURIComplete(val url.URL) { + s.VerificationURIComplete = val +} + +// SetExpiresIn sets the value of ExpiresIn. +func (s *CliLoginStartResult) SetExpiresIn(val int) { + s.ExpiresIn = val +} + +// SetInterval sets the value of Interval. +func (s *CliLoginStartResult) SetInterval(val int) { + s.Interval = val +} + +type CliLogoutBadRequest ErrorResponse + +func (*CliLogoutBadRequest) cliLogoutRes() {} + +type CliLogoutForbidden ErrorResponse + +func (*CliLogoutForbidden) cliLogoutRes() {} + +// Ref: #/components/schemas/CliLogoutInput +type CliLogoutInput struct { + // Optional key id guard; when provided it must match the authenticated API key. + KeyID OptUUID `json:"key_id"` +} + +// GetKeyID returns the value of KeyID. +func (s *CliLogoutInput) GetKeyID() OptUUID { + return s.KeyID +} + +// SetKeyID sets the value of KeyID. +func (s *CliLogoutInput) SetKeyID(val OptUUID) { + s.KeyID = val +} + +type CliLogoutNotFound ErrorResponse + +func (*CliLogoutNotFound) cliLogoutRes() {} + +// Merged schema. +type CliLogoutOK struct { + Success bool `json:"success"` + Data CliLogoutResult `json:"data"` +} + +// GetSuccess returns the value of Success. +func (s *CliLogoutOK) GetSuccess() bool { + return s.Success +} + +// GetData returns the value of Data. +func (s *CliLogoutOK) GetData() CliLogoutResult { + return s.Data +} + +// SetSuccess sets the value of Success. +func (s *CliLogoutOK) SetSuccess(val bool) { + s.Success = val +} + +// SetData sets the value of Data. +func (s *CliLogoutOK) SetData(val CliLogoutResult) { + s.Data = val +} + +func (*CliLogoutOK) cliLogoutRes() {} + +// Ref: #/components/schemas/CliLogoutResult +type CliLogoutResult struct { + Revoked bool `json:"revoked"` + KeyID uuid.UUID `json:"key_id"` +} + +// GetRevoked returns the value of Revoked. +func (s *CliLogoutResult) GetRevoked() bool { + return s.Revoked +} + +// GetKeyID returns the value of KeyID. +func (s *CliLogoutResult) GetKeyID() uuid.UUID { + return s.KeyID +} + +// SetRevoked sets the value of Revoked. +func (s *CliLogoutResult) SetRevoked(val bool) { + s.Revoked = val +} + +// SetKeyID sets the value of KeyID. +func (s *CliLogoutResult) SetKeyID(val uuid.UUID) { + s.KeyID = val +} + +type CliLogoutUnauthorized ErrorResponse + +func (*CliLogoutUnauthorized) cliLogoutRes() {} + type CreateEndpointBadRequest ErrorResponse func (*CreateEndpointBadRequest) createEndpointRes() {} @@ -2382,6 +2605,7 @@ func (*ErrorResponse) getSendPermissionsRes() {} func (*ErrorResponse) listDomainsRes() {} func (*ErrorResponse) listEndpointsRes() {} func (*ErrorResponse) listFiltersRes() {} +func (*ErrorResponse) startCliLoginRes() {} type ErrorResponseError struct { Code ErrorResponseErrorCode `json:"code"` @@ -2468,6 +2692,11 @@ const ( ErrorResponseErrorCodeOutboundRelayFailed ErrorResponseErrorCode = "outbound_relay_failed" ErrorResponseErrorCodeDiscardNotEnabled ErrorResponseErrorCode = "discard_not_enabled" ErrorResponseErrorCodeInboundNotRepliable ErrorResponseErrorCode = "inbound_not_repliable" + ErrorResponseErrorCodeAuthorizationPending ErrorResponseErrorCode = "authorization_pending" + ErrorResponseErrorCodeSlowDown ErrorResponseErrorCode = "slow_down" + ErrorResponseErrorCodeAccessDenied ErrorResponseErrorCode = "access_denied" + ErrorResponseErrorCodeExpiredToken ErrorResponseErrorCode = "expired_token" + ErrorResponseErrorCodeInvalidDeviceCode ErrorResponseErrorCode = "invalid_device_code" ) // AllValues returns all ErrorResponseErrorCode values. @@ -2492,6 +2721,11 @@ func (ErrorResponseErrorCode) AllValues() []ErrorResponseErrorCode { ErrorResponseErrorCodeOutboundRelayFailed, ErrorResponseErrorCodeDiscardNotEnabled, ErrorResponseErrorCodeInboundNotRepliable, + ErrorResponseErrorCodeAuthorizationPending, + ErrorResponseErrorCodeSlowDown, + ErrorResponseErrorCodeAccessDenied, + ErrorResponseErrorCodeExpiredToken, + ErrorResponseErrorCodeInvalidDeviceCode, } } @@ -2536,6 +2770,16 @@ func (s ErrorResponseErrorCode) MarshalText() ([]byte, error) { return []byte(s), nil case ErrorResponseErrorCodeInboundNotRepliable: return []byte(s), nil + case ErrorResponseErrorCodeAuthorizationPending: + return []byte(s), nil + case ErrorResponseErrorCodeSlowDown: + return []byte(s), nil + case ErrorResponseErrorCodeAccessDenied: + return []byte(s), nil + case ErrorResponseErrorCodeExpiredToken: + return []byte(s), nil + case ErrorResponseErrorCodeInvalidDeviceCode: + return []byte(s), nil default: return nil, errors.Errorf("invalid value: %q", s) } @@ -2601,6 +2845,21 @@ func (s *ErrorResponseErrorCode) UnmarshalText(data []byte) error { case ErrorResponseErrorCodeInboundNotRepliable: *s = ErrorResponseErrorCodeInboundNotRepliable return nil + case ErrorResponseErrorCodeAuthorizationPending: + *s = ErrorResponseErrorCodeAuthorizationPending + return nil + case ErrorResponseErrorCodeSlowDown: + *s = ErrorResponseErrorCodeSlowDown + return nil + case ErrorResponseErrorCodeAccessDenied: + *s = ErrorResponseErrorCodeAccessDenied + return nil + case ErrorResponseErrorCodeExpiredToken: + *s = ErrorResponseErrorCodeExpiredToken + return nil + case ErrorResponseErrorCodeInvalidDeviceCode: + *s = ErrorResponseErrorCodeInvalidDeviceCode + return nil default: return errors.Errorf("invalid value: %q", data) } @@ -2722,6 +2981,34 @@ func (s *ErrorResponseErrorDetailsMxConflict) SetSuggestedSubdomain(val string) s.SuggestedSubdomain = val } +// ErrorResponseHeaders wraps ErrorResponse with response headers. +type ErrorResponseHeaders struct { + RetryAfter OptInt + Response ErrorResponse +} + +// GetRetryAfter returns the value of RetryAfter. +func (s *ErrorResponseHeaders) GetRetryAfter() OptInt { + return s.RetryAfter +} + +// GetResponse returns the value of Response. +func (s *ErrorResponseHeaders) GetResponse() ErrorResponse { + return s.Response +} + +// SetRetryAfter sets the value of RetryAfter. +func (s *ErrorResponseHeaders) SetRetryAfter(val OptInt) { + s.RetryAfter = val +} + +// SetResponse sets the value of Response. +func (s *ErrorResponseHeaders) SetResponse(val ErrorResponse) { + s.Response = val +} + +func (*ErrorResponseHeaders) pollCliLoginRes() {} + // Ref: #/components/schemas/Filter type Filter struct { ID uuid.UUID `json:"id"` @@ -3687,6 +3974,52 @@ func (o OptBool) Or(d bool) bool { return d } +// NewOptCliLogoutInput returns new OptCliLogoutInput with value set to v. +func NewOptCliLogoutInput(v CliLogoutInput) OptCliLogoutInput { + return OptCliLogoutInput{ + Value: v, + Set: true, + } +} + +// OptCliLogoutInput is optional CliLogoutInput. +type OptCliLogoutInput struct { + Value CliLogoutInput + Set bool +} + +// IsSet returns true if OptCliLogoutInput was set. +func (o OptCliLogoutInput) IsSet() bool { return o.Set } + +// Reset unsets value. +func (o *OptCliLogoutInput) Reset() { + var v CliLogoutInput + o.Value = v + o.Set = false +} + +// SetTo sets value to v. +func (o *OptCliLogoutInput) SetTo(v CliLogoutInput) { + o.Set = true + o.Value = v +} + +// Get returns value and boolean that denotes whether value was set. +func (o OptCliLogoutInput) Get() (v CliLogoutInput, ok bool) { + if !o.Set { + return v, false + } + return o.Value, true +} + +// Or returns value if set, or given parameter if does not. +func (o OptCliLogoutInput) Or(d CliLogoutInput) CliLogoutInput { + if v, ok := o.Get(); ok { + return v + } + return d +} + // NewOptDateTime returns new OptDateTime with value set to v. func NewOptDateTime(v time.Time) OptDateTime { return OptDateTime{ @@ -4731,6 +5064,98 @@ func (o OptSentEmailStatus) Or(d SentEmailStatus) SentEmailStatus { return d } +// NewOptStartCliLoginInput returns new OptStartCliLoginInput with value set to v. +func NewOptStartCliLoginInput(v StartCliLoginInput) OptStartCliLoginInput { + return OptStartCliLoginInput{ + Value: v, + Set: true, + } +} + +// OptStartCliLoginInput is optional StartCliLoginInput. +type OptStartCliLoginInput struct { + Value StartCliLoginInput + Set bool +} + +// IsSet returns true if OptStartCliLoginInput was set. +func (o OptStartCliLoginInput) IsSet() bool { return o.Set } + +// Reset unsets value. +func (o *OptStartCliLoginInput) Reset() { + var v StartCliLoginInput + o.Value = v + o.Set = false +} + +// SetTo sets value to v. +func (o *OptStartCliLoginInput) SetTo(v StartCliLoginInput) { + o.Set = true + o.Value = v +} + +// Get returns value and boolean that denotes whether value was set. +func (o OptStartCliLoginInput) Get() (v StartCliLoginInput, ok bool) { + if !o.Set { + return v, false + } + return o.Value, true +} + +// Or returns value if set, or given parameter if does not. +func (o OptStartCliLoginInput) Or(d StartCliLoginInput) StartCliLoginInput { + if v, ok := o.Get(); ok { + return v + } + return d +} + +// NewOptStartCliLoginInputMetadata returns new OptStartCliLoginInputMetadata with value set to v. +func NewOptStartCliLoginInputMetadata(v StartCliLoginInputMetadata) OptStartCliLoginInputMetadata { + return OptStartCliLoginInputMetadata{ + Value: v, + Set: true, + } +} + +// OptStartCliLoginInputMetadata is optional StartCliLoginInputMetadata. +type OptStartCliLoginInputMetadata struct { + Value StartCliLoginInputMetadata + Set bool +} + +// IsSet returns true if OptStartCliLoginInputMetadata was set. +func (o OptStartCliLoginInputMetadata) IsSet() bool { return o.Set } + +// Reset unsets value. +func (o *OptStartCliLoginInputMetadata) Reset() { + var v StartCliLoginInputMetadata + o.Value = v + o.Set = false +} + +// SetTo sets value to v. +func (o *OptStartCliLoginInputMetadata) SetTo(v StartCliLoginInputMetadata) { + o.Set = true + o.Value = v +} + +// Get returns value and boolean that denotes whether value was set. +func (o OptStartCliLoginInputMetadata) Get() (v StartCliLoginInputMetadata, ok bool) { + if !o.Set { + return v, false + } + return o.Value, true +} + +// Or returns value if set, or given parameter if does not. +func (o OptStartCliLoginInputMetadata) Or(d StartCliLoginInputMetadata) StartCliLoginInputMetadata { + if v, ok := o.Get(); ok { + return v + } + return d +} + // NewOptString returns new OptString with value set to v. func NewOptString(v string) OptString { return OptString{ @@ -4863,6 +5288,83 @@ func (s *PaginationMeta) SetCursor(val NilString) { s.Cursor = val } +type PollCliLoginBadRequest ErrorResponse + +func (*PollCliLoginBadRequest) pollCliLoginRes() {} + +type PollCliLoginForbidden ErrorResponse + +func (*PollCliLoginForbidden) pollCliLoginRes() {} + +// Ref: #/components/schemas/PollCliLoginInput +type PollCliLoginInput struct { + DeviceCode string `json:"device_code"` +} + +// GetDeviceCode returns the value of DeviceCode. +func (s *PollCliLoginInput) GetDeviceCode() string { + return s.DeviceCode +} + +// SetDeviceCode sets the value of DeviceCode. +func (s *PollCliLoginInput) SetDeviceCode(val string) { + s.DeviceCode = val +} + +// Merged schema. +type PollCliLoginOK struct { + Success bool `json:"success"` + Data CliLoginPollResult `json:"data"` +} + +// GetSuccess returns the value of Success. +func (s *PollCliLoginOK) GetSuccess() bool { + return s.Success +} + +// GetData returns the value of Data. +func (s *PollCliLoginOK) GetData() CliLoginPollResult { + return s.Data +} + +// SetSuccess sets the value of Success. +func (s *PollCliLoginOK) SetSuccess(val bool) { + s.Success = val +} + +// SetData sets the value of Data. +func (s *PollCliLoginOK) SetData(val CliLoginPollResult) { + s.Data = val +} + +// PollCliLoginOKHeaders wraps PollCliLoginOK with response headers. +type PollCliLoginOKHeaders struct { + CacheControl OptString + Response PollCliLoginOK +} + +// GetCacheControl returns the value of CacheControl. +func (s *PollCliLoginOKHeaders) GetCacheControl() OptString { + return s.CacheControl +} + +// GetResponse returns the value of Response. +func (s *PollCliLoginOKHeaders) GetResponse() PollCliLoginOK { + return s.Response +} + +// SetCacheControl sets the value of CacheControl. +func (s *PollCliLoginOKHeaders) SetCacheControl(val OptString) { + s.CacheControl = val +} + +// SetResponse sets the value of Response. +func (s *PollCliLoginOKHeaders) SetResponse(val PollCliLoginOK) { + s.Response = val +} + +func (*PollCliLoginOKHeaders) pollCliLoginRes() {} + // RateLimitedHeaders wraps ErrorResponse with response headers. type RateLimitedHeaders struct { RetryAfter OptInt @@ -4895,6 +5397,7 @@ func (*RateLimitedHeaders) replayEmailWebhooksRes() {} func (*RateLimitedHeaders) replyToEmailRes() {} func (*RateLimitedHeaders) rotateWebhookSecretRes() {} func (*RateLimitedHeaders) sendEmailRes() {} +func (*RateLimitedHeaders) startCliLoginRes() {} func (*RateLimitedHeaders) testEndpointRes() {} type ReplayDeliveryBadRequest ErrorResponse @@ -6920,6 +7423,100 @@ func (s *SentEmailSummary) SetRequestID(val OptNilString) { s.RequestID = val } +// Merged schema. +type StartCliLoginCreated struct { + Success bool `json:"success"` + Data CliLoginStartResult `json:"data"` +} + +// GetSuccess returns the value of Success. +func (s *StartCliLoginCreated) GetSuccess() bool { + return s.Success +} + +// GetData returns the value of Data. +func (s *StartCliLoginCreated) GetData() CliLoginStartResult { + return s.Data +} + +// SetSuccess sets the value of Success. +func (s *StartCliLoginCreated) SetSuccess(val bool) { + s.Success = val +} + +// SetData sets the value of Data. +func (s *StartCliLoginCreated) SetData(val CliLoginStartResult) { + s.Data = val +} + +// StartCliLoginCreatedHeaders wraps StartCliLoginCreated with response headers. +type StartCliLoginCreatedHeaders struct { + CacheControl OptString + Response StartCliLoginCreated +} + +// GetCacheControl returns the value of CacheControl. +func (s *StartCliLoginCreatedHeaders) GetCacheControl() OptString { + return s.CacheControl +} + +// GetResponse returns the value of Response. +func (s *StartCliLoginCreatedHeaders) GetResponse() StartCliLoginCreated { + return s.Response +} + +// SetCacheControl sets the value of CacheControl. +func (s *StartCliLoginCreatedHeaders) SetCacheControl(val OptString) { + s.CacheControl = val +} + +// SetResponse sets the value of Response. +func (s *StartCliLoginCreatedHeaders) SetResponse(val StartCliLoginCreated) { + s.Response = val +} + +func (*StartCliLoginCreatedHeaders) startCliLoginRes() {} + +// Ref: #/components/schemas/StartCliLoginInput +type StartCliLoginInput struct { + // Human-readable device name shown during browser approval. + DeviceName OptString `json:"device_name"` + // Optional client metadata stored with the login session. + Metadata OptStartCliLoginInputMetadata `json:"metadata"` +} + +// GetDeviceName returns the value of DeviceName. +func (s *StartCliLoginInput) GetDeviceName() OptString { + return s.DeviceName +} + +// GetMetadata returns the value of Metadata. +func (s *StartCliLoginInput) GetMetadata() OptStartCliLoginInputMetadata { + return s.Metadata +} + +// SetDeviceName sets the value of DeviceName. +func (s *StartCliLoginInput) SetDeviceName(val OptString) { + s.DeviceName = val +} + +// SetMetadata sets the value of Metadata. +func (s *StartCliLoginInput) SetMetadata(val OptStartCliLoginInputMetadata) { + s.Metadata = val +} + +// Optional client metadata stored with the login session. +type StartCliLoginInputMetadata map[string]jx.Raw + +func (s *StartCliLoginInputMetadata) init() StartCliLoginInputMetadata { + m := *s + if m == nil { + m = map[string]jx.Raw{} + *s = m + } + return m +} + // Ref: #/components/schemas/StorageStats type StorageStats struct { // Total storage used in bytes. diff --git a/sdk-go/api/oas_security_gen.go b/sdk-go/api/oas_security_gen.go index a72dce4..5043639 100644 --- a/sdk-go/api/oas_security_gen.go +++ b/sdk-go/api/oas_security_gen.go @@ -39,6 +39,7 @@ func findAuthorization(h http.Header, prefix string) (string, bool) { // operationRolesBearerAuth is a private map storing roles per operation. var operationRolesBearerAuth = map[string][]string{ AddDomainOperation: []string{}, + CliLogoutOperation: []string{}, CreateEndpointOperation: []string{}, CreateFilterOperation: []string{}, DeleteDomainOperation: []string{}, diff --git a/sdk-go/api/oas_server_gen.go b/sdk-go/api/oas_server_gen.go index 539c48d..456764e 100644 --- a/sdk-go/api/oas_server_gen.go +++ b/sdk-go/api/oas_server_gen.go @@ -16,6 +16,13 @@ type Handler interface { // // POST /domains AddDomain(ctx context.Context, req *AddDomainInput) (AddDomainRes, error) + // CliLogout implements cliLogout operation. + // + // Revokes the API key used to authenticate the request. CLI clients use + // this endpoint during `primitive logout` before removing local credentials. + // + // POST /cli/logout + CliLogout(ctx context.Context, req OptCliLogoutInput) (CliLogoutRes, error) // CreateEndpoint implements createEndpoint operation. // // Creates a new webhook endpoint. If a deactivated endpoint @@ -264,6 +271,14 @@ type Handler interface { // // GET /sent-emails ListSentEmails(ctx context.Context, params ListSentEmailsParams) (ListSentEmailsRes, error) + // PollCliLogin implements pollCliLogin operation. + // + // Polls a CLI login session until the browser approval either succeeds, + // is denied, expires, or is polled too quickly. The API key is generated + // only after approval and is returned exactly once. + // + // POST /cli/login/poll + PollCliLogin(ctx context.Context, req *PollCliLoginInput) (PollCliLoginRes, error) // ReplayDelivery implements replayDelivery operation. // // Re-sends the stored webhook payload from a previous delivery attempt. @@ -315,6 +330,14 @@ type Handler interface { // // POST /send-mail SendEmail(ctx context.Context, req *SendMailInput, params SendEmailParams) (SendEmailRes, error) + // StartCliLogin implements startCliLogin operation. + // + // Starts a browser-assisted CLI login session. The response includes a + // device code for polling and a user code that the user approves in the + // browser. This endpoint does not require an API key. + // + // POST /cli/login/start + StartCliLogin(ctx context.Context, req OptStartCliLoginInput) (StartCliLoginRes, error) // TestEndpoint implements testEndpoint operation. // // Sends a sample `email.received` event to the endpoint. The request diff --git a/sdk-go/api/oas_unimplemented_gen.go b/sdk-go/api/oas_unimplemented_gen.go index 85384ff..a04ab21 100644 --- a/sdk-go/api/oas_unimplemented_gen.go +++ b/sdk-go/api/oas_unimplemented_gen.go @@ -24,6 +24,16 @@ func (UnimplementedHandler) AddDomain(ctx context.Context, req *AddDomainInput) return r, ht.ErrNotImplemented } +// CliLogout implements cliLogout operation. +// +// Revokes the API key used to authenticate the request. CLI clients use +// this endpoint during `primitive logout` before removing local credentials. +// +// POST /cli/logout +func (UnimplementedHandler) CliLogout(ctx context.Context, req OptCliLogoutInput) (r CliLogoutRes, _ error) { + return r, ht.ErrNotImplemented +} + // CreateEndpoint implements createEndpoint operation. // // Creates a new webhook endpoint. If a deactivated endpoint @@ -335,6 +345,17 @@ func (UnimplementedHandler) ListSentEmails(ctx context.Context, params ListSentE return r, ht.ErrNotImplemented } +// PollCliLogin implements pollCliLogin operation. +// +// Polls a CLI login session until the browser approval either succeeds, +// is denied, expires, or is polled too quickly. The API key is generated +// only after approval and is returned exactly once. +// +// POST /cli/login/poll +func (UnimplementedHandler) PollCliLogin(ctx context.Context, req *PollCliLoginInput) (r PollCliLoginRes, _ error) { + return r, ht.ErrNotImplemented +} + // ReplayDelivery implements replayDelivery operation. // // Re-sends the stored webhook payload from a previous delivery attempt. @@ -401,6 +422,17 @@ func (UnimplementedHandler) SendEmail(ctx context.Context, req *SendMailInput, p return r, ht.ErrNotImplemented } +// StartCliLogin implements startCliLogin operation. +// +// Starts a browser-assisted CLI login session. The response includes a +// device code for polling and a user code that the user approves in the +// browser. This endpoint does not require an API key. +// +// POST /cli/login/start +func (UnimplementedHandler) StartCliLogin(ctx context.Context, req OptStartCliLoginInput) (r StartCliLoginRes, _ error) { + return r, ht.ErrNotImplemented +} + // TestEndpoint implements testEndpoint operation. // // Sends a sample `email.received` event to the endpoint. The request diff --git a/sdk-go/api/oas_validators_gen.go b/sdk-go/api/oas_validators_gen.go index 43ffdbe..4facc38 100644 --- a/sdk-go/api/oas_validators_gen.go +++ b/sdk-go/api/oas_validators_gen.go @@ -148,6 +148,73 @@ func (s *AddDomainUnauthorized) Validate() error { return nil } +func (s *CliLoginStartResult) Validate() error { + if s == nil { + return validate.ErrNilPointer + } + + var failures []validate.FieldError + if err := func() error { + if err := (validate.String{ + MinLength: 0, + MinLengthSet: false, + MaxLength: 0, + MaxLengthSet: false, + Email: false, + Hostname: false, + Regex: regexMap["^[BCDFGHJKLMNPQRSTVWXZ]{4}-[BCDFGHJKLMNPQRSTVWXZ]{4}$"], + MinNumeric: 0, + MinNumericSet: false, + MaxNumeric: 0, + MaxNumericSet: false, + }).Validate(string(s.UserCode)); err != nil { + return errors.Wrap(err, "string") + } + return nil + }(); err != nil { + failures = append(failures, validate.FieldError{ + Name: "user_code", + Error: err, + }) + } + if len(failures) > 0 { + return &validate.Error{Fields: failures} + } + return nil +} + +func (s *CliLogoutBadRequest) Validate() error { + alias := (*ErrorResponse)(s) + if err := alias.Validate(); err != nil { + return err + } + return nil +} + +func (s *CliLogoutForbidden) Validate() error { + alias := (*ErrorResponse)(s) + if err := alias.Validate(); err != nil { + return err + } + return nil +} + +func (s *CliLogoutNotFound) Validate() error { + alias := (*ErrorResponse)(s) + if err := alias.Validate(); err != nil { + return err + } + return nil +} + +func (s *CliLogoutUnauthorized) Validate() error { + alias := (*ErrorResponse)(s) + if err := alias.Validate(); err != nil { + return err + } + return nil +} + func (s *CreateEndpointBadRequest) Validate() error { alias := (*ErrorResponse)(s) if err := alias.Validate(); err != nil { @@ -884,11 +951,44 @@ func (s ErrorResponseErrorCode) Validate() error { return nil case "inbound_not_repliable": return nil + case "authorization_pending": + return nil + case "slow_down": + return nil + case "access_denied": + return nil + case "expired_token": + return nil + case "invalid_device_code": + return nil default: return errors.Errorf("invalid value: %v", s) } } +func (s *ErrorResponseHeaders) Validate() error { + if s == nil { + return validate.ErrNilPointer + } + + var failures []validate.FieldError + if err := func() error { + if err := s.Response.Validate(); err != nil { + return err + } + return nil + }(); err != nil { + failures = append(failures, validate.FieldError{ + Name: "Response", + Error: err, + }) + } + if len(failures) > 0 { + return &validate.Error{Fields: failures} + } + return nil +} + func (s *Filter) Validate() error { if s == nil { return validate.ErrNilPointer @@ -1549,6 +1649,57 @@ func (s *ListSentEmailsUnauthorized) Validate() error { return nil } +func (s *PollCliLoginBadRequest) Validate() error { + alias := (*ErrorResponse)(s) + if err := alias.Validate(); err != nil { + return err + } + return nil +} + +func (s *PollCliLoginForbidden) Validate() error { + alias := (*ErrorResponse)(s) + if err := alias.Validate(); err != nil { + return err + } + return nil +} + +func (s *PollCliLoginInput) Validate() error { + if s == nil { + return validate.ErrNilPointer + } + + var failures []validate.FieldError + if err := func() error { + if err := (validate.String{ + MinLength: 1, + MinLengthSet: true, + MaxLength: 0, + MaxLengthSet: false, + Email: false, + Hostname: false, + Regex: nil, + MinNumeric: 0, + MinNumericSet: false, + MaxNumeric: 0, + MaxNumericSet: false, + }).Validate(string(s.DeviceCode)); err != nil { + return errors.Wrap(err, "string") + } + return nil + }(); err != nil { + failures = append(failures, validate.FieldError{ + Name: "device_code", + Error: err, + }) + } + if len(failures) > 0 { + return &validate.Error{Fields: failures} + } + return nil +} + func (s *RateLimitedHeaders) Validate() error { if s == nil { return validate.ErrNilPointer @@ -2390,6 +2541,94 @@ func (s *SentEmailSummary) Validate() error { return nil } +func (s *StartCliLoginCreated) Validate() error { + if s == nil { + return validate.ErrNilPointer + } + + var failures []validate.FieldError + if err := func() error { + if err := s.Data.Validate(); err != nil { + return err + } + return nil + }(); err != nil { + failures = append(failures, validate.FieldError{ + Name: "data", + Error: err, + }) + } + if len(failures) > 0 { + return &validate.Error{Fields: failures} + } + return nil +} + +func (s *StartCliLoginCreatedHeaders) Validate() error { + if s == nil { + return validate.ErrNilPointer + } + + var failures []validate.FieldError + if err := func() error { + if err := s.Response.Validate(); err != nil { + return err + } + return nil + }(); err != nil { + failures = append(failures, validate.FieldError{ + Name: "Response", + Error: err, + }) + } + if len(failures) > 0 { + return &validate.Error{Fields: failures} + } + return nil +} + +func (s *StartCliLoginInput) Validate() error { + if s == nil { + return validate.ErrNilPointer + } + + var failures []validate.FieldError + if err := func() error { + if value, ok := s.DeviceName.Get(); ok { + if err := func() error { + if err := (validate.String{ + MinLength: 1, + MinLengthSet: true, + MaxLength: 80, + MaxLengthSet: true, + Email: false, + Hostname: false, + Regex: nil, + MinNumeric: 0, + MinNumericSet: false, + MaxNumeric: 0, + MaxNumericSet: false, + }).Validate(string(value)); err != nil { + return errors.Wrap(err, "string") + } + return nil + }(); err != nil { + return err + } + } + return nil + }(); err != nil { + failures = append(failures, validate.FieldError{ + Name: "device_name", + Error: err, + }) + } + if len(failures) > 0 { + return &validate.Error{Fields: failures} + } + return nil +} + func (s *StorageStats) Validate() error { if s == nil { return validate.ErrNilPointer diff --git a/sdk-node/package.json b/sdk-node/package.json index c8100d3..ee59df3 100644 --- a/sdk-node/package.json +++ b/sdk-node/package.json @@ -59,6 +59,9 @@ "@oclif/plugin-autocomplete" ], "topics": { + "cli": { + "description": "Browser-assisted CLI authentication" + }, "account": { "description": "Manage your account settings, storage, and webhook secret" }, diff --git a/sdk-node/src/api/generated/index.ts b/sdk-node/src/api/generated/index.ts index 6678040..6b5e732 100644 --- a/sdk-node/src/api/generated/index.ts +++ b/sdk-node/src/api/generated/index.ts @@ -1,4 +1,4 @@ // This file is auto-generated by @hey-api/openapi-ts -export { addDomain, createEndpoint, createFilter, deleteDomain, deleteEmail, deleteEndpoint, deleteFilter, discardEmailContent, downloadAttachments, downloadRawEmail, getAccount, getEmail, getSendPermissions, getSentEmail, getStorageStats, getWebhookSecret, listDeliveries, listDomains, listEmails, listEndpoints, listFilters, listSentEmails, type Options, replayDelivery, replayEmailWebhooks, replyToEmail, rotateWebhookSecret, sendEmail, testEndpoint, updateAccount, updateDomain, updateEndpoint, updateFilter, verifyDomain } from './sdk.gen.js'; -export type { Account, AccountUpdated, AddDomainData, AddDomainError, AddDomainErrors, AddDomainInput, AddDomainResponse, AddDomainResponses, ClientOptions, CreateEndpointData, CreateEndpointError, CreateEndpointErrors, CreateEndpointInput, CreateEndpointResponse, CreateEndpointResponses, CreateFilterData, CreateFilterError, CreateFilterErrors, CreateFilterInput, CreateFilterResponse, CreateFilterResponses, Cursor, DeleteDomainData, DeleteDomainError, DeleteDomainErrors, DeleteDomainResponse, DeleteDomainResponses, DeleteEmailData, DeleteEmailError, DeleteEmailErrors, DeleteEmailResponse, DeleteEmailResponses, DeleteEndpointData, DeleteEndpointError, DeleteEndpointErrors, DeleteEndpointResponse, DeleteEndpointResponses, DeleteFilterData, DeleteFilterError, DeleteFilterErrors, DeleteFilterResponse, DeleteFilterResponses, DeliveryStatus, DeliverySummary, DiscardContentResult, DiscardEmailContentData, DiscardEmailContentError, DiscardEmailContentErrors, DiscardEmailContentResponse, DiscardEmailContentResponses, Domain, DomainVerifyResult, DownloadAttachmentsData, DownloadAttachmentsError, DownloadAttachmentsErrors, DownloadAttachmentsResponse, DownloadAttachmentsResponses, DownloadRawEmailData, DownloadRawEmailError, DownloadRawEmailErrors, DownloadRawEmailResponse, DownloadRawEmailResponses, EmailDetail, EmailDetailReply, EmailStatus, EmailSummary, EmailWebhookStatus, Endpoint, ErrorResponse, Filter, GateDenial, GateFix, GetAccountData, GetAccountError, GetAccountErrors, GetAccountResponse, GetAccountResponses, GetEmailData, GetEmailError, GetEmailErrors, GetEmailResponse, GetEmailResponses, GetSendPermissionsData, GetSendPermissionsError, GetSendPermissionsErrors, GetSendPermissionsResponse, GetSendPermissionsResponses, GetSentEmailData, GetSentEmailError, GetSentEmailErrors, GetSentEmailResponse, GetSentEmailResponses, GetStorageStatsData, GetStorageStatsError, GetStorageStatsErrors, GetStorageStatsResponse, GetStorageStatsResponses, GetWebhookSecretData, GetWebhookSecretError, GetWebhookSecretErrors, GetWebhookSecretResponse, GetWebhookSecretResponses, Limit, ListDeliveriesData, ListDeliveriesError, ListDeliveriesErrors, ListDeliveriesResponse, ListDeliveriesResponses, ListDomainsData, ListDomainsError, ListDomainsErrors, ListDomainsResponse, ListDomainsResponses, ListEmailsData, ListEmailsError, ListEmailsErrors, ListEmailsResponse, ListEmailsResponses, ListEndpointsData, ListEndpointsError, ListEndpointsErrors, ListEndpointsResponse, ListEndpointsResponses, ListEnvelope, ListFiltersData, ListFiltersError, ListFiltersErrors, ListFiltersResponse, ListFiltersResponses, ListSentEmailsData, ListSentEmailsError, ListSentEmailsErrors, ListSentEmailsResponse, ListSentEmailsResponses, PaginationMeta, ReplayDeliveryData, ReplayDeliveryError, ReplayDeliveryErrors, ReplayDeliveryResponse, ReplayDeliveryResponses, ReplayEmailWebhooksData, ReplayEmailWebhooksError, ReplayEmailWebhooksErrors, ReplayEmailWebhooksResponse, ReplayEmailWebhooksResponses, ReplayResult, ReplyInput, ReplyToEmailData, ReplyToEmailError, ReplyToEmailErrors, ReplyToEmailResponse, ReplyToEmailResponses, ResourceId, RotateWebhookSecretData, RotateWebhookSecretError, RotateWebhookSecretErrors, RotateWebhookSecretResponse, RotateWebhookSecretResponses, SendEmailData, SendEmailError, SendEmailErrors, SendEmailResponse, SendEmailResponses, SendMailInput, SendMailResult, SendPermissionAddress, SendPermissionAnyRecipient, SendPermissionManagedZone, SendPermissionRule, SendPermissionsMeta, SendPermissionYourDomain, SentEmailDetail, SentEmailStatus, SentEmailSummary, StorageStats, SuccessEnvelope, TestEndpointData, TestEndpointError, TestEndpointErrors, TestEndpointResponse, TestEndpointResponses, TestResult, UnverifiedDomain, UpdateAccountData, UpdateAccountError, UpdateAccountErrors, UpdateAccountInput, UpdateAccountResponse, UpdateAccountResponses, UpdateDomainData, UpdateDomainError, UpdateDomainErrors, UpdateDomainInput, UpdateDomainResponse, UpdateDomainResponses, UpdateEndpointData, UpdateEndpointError, UpdateEndpointErrors, UpdateEndpointInput, UpdateEndpointResponse, UpdateEndpointResponses, UpdateFilterData, UpdateFilterError, UpdateFilterErrors, UpdateFilterInput, UpdateFilterResponse, UpdateFilterResponses, VerifiedDomain, VerifyDomainData, VerifyDomainError, VerifyDomainErrors, VerifyDomainResponse, VerifyDomainResponses, WebhookSecret } from './types.gen.js'; +export { addDomain, cliLogout, createEndpoint, createFilter, deleteDomain, deleteEmail, deleteEndpoint, deleteFilter, discardEmailContent, downloadAttachments, downloadRawEmail, getAccount, getEmail, getSendPermissions, getSentEmail, getStorageStats, getWebhookSecret, listDeliveries, listDomains, listEmails, listEndpoints, listFilters, listSentEmails, type Options, pollCliLogin, replayDelivery, replayEmailWebhooks, replyToEmail, rotateWebhookSecret, sendEmail, startCliLogin, testEndpoint, updateAccount, updateDomain, updateEndpoint, updateFilter, verifyDomain } from './sdk.gen.js'; +export type { Account, AccountUpdated, AddDomainData, AddDomainError, AddDomainErrors, AddDomainInput, AddDomainResponse, AddDomainResponses, ClientOptions, CliLoginPollResult, CliLoginStartResult, CliLogoutData, CliLogoutError, CliLogoutErrors, CliLogoutInput, CliLogoutResponse, CliLogoutResponses, CliLogoutResult, CreateEndpointData, CreateEndpointError, CreateEndpointErrors, CreateEndpointInput, CreateEndpointResponse, CreateEndpointResponses, CreateFilterData, CreateFilterError, CreateFilterErrors, CreateFilterInput, CreateFilterResponse, CreateFilterResponses, Cursor, DeleteDomainData, DeleteDomainError, DeleteDomainErrors, DeleteDomainResponse, DeleteDomainResponses, DeleteEmailData, DeleteEmailError, DeleteEmailErrors, DeleteEmailResponse, DeleteEmailResponses, DeleteEndpointData, DeleteEndpointError, DeleteEndpointErrors, DeleteEndpointResponse, DeleteEndpointResponses, DeleteFilterData, DeleteFilterError, DeleteFilterErrors, DeleteFilterResponse, DeleteFilterResponses, DeliveryStatus, DeliverySummary, DiscardContentResult, DiscardEmailContentData, DiscardEmailContentError, DiscardEmailContentErrors, DiscardEmailContentResponse, DiscardEmailContentResponses, Domain, DomainVerifyResult, DownloadAttachmentsData, DownloadAttachmentsError, DownloadAttachmentsErrors, DownloadAttachmentsResponse, DownloadAttachmentsResponses, DownloadRawEmailData, DownloadRawEmailError, DownloadRawEmailErrors, DownloadRawEmailResponse, DownloadRawEmailResponses, EmailDetail, EmailDetailReply, EmailStatus, EmailSummary, EmailWebhookStatus, Endpoint, ErrorResponse, Filter, GateDenial, GateFix, GetAccountData, GetAccountError, GetAccountErrors, GetAccountResponse, GetAccountResponses, GetEmailData, GetEmailError, GetEmailErrors, GetEmailResponse, GetEmailResponses, GetSendPermissionsData, GetSendPermissionsError, GetSendPermissionsErrors, GetSendPermissionsResponse, GetSendPermissionsResponses, GetSentEmailData, GetSentEmailError, GetSentEmailErrors, GetSentEmailResponse, GetSentEmailResponses, GetStorageStatsData, GetStorageStatsError, GetStorageStatsErrors, GetStorageStatsResponse, GetStorageStatsResponses, GetWebhookSecretData, GetWebhookSecretError, GetWebhookSecretErrors, GetWebhookSecretResponse, GetWebhookSecretResponses, Limit, ListDeliveriesData, ListDeliveriesError, ListDeliveriesErrors, ListDeliveriesResponse, ListDeliveriesResponses, ListDomainsData, ListDomainsError, ListDomainsErrors, ListDomainsResponse, ListDomainsResponses, ListEmailsData, ListEmailsError, ListEmailsErrors, ListEmailsResponse, ListEmailsResponses, ListEndpointsData, ListEndpointsError, ListEndpointsErrors, ListEndpointsResponse, ListEndpointsResponses, ListEnvelope, ListFiltersData, ListFiltersError, ListFiltersErrors, ListFiltersResponse, ListFiltersResponses, ListSentEmailsData, ListSentEmailsError, ListSentEmailsErrors, ListSentEmailsResponse, ListSentEmailsResponses, PaginationMeta, PollCliLoginData, PollCliLoginError, PollCliLoginErrors, PollCliLoginInput, PollCliLoginResponse, PollCliLoginResponses, ReplayDeliveryData, ReplayDeliveryError, ReplayDeliveryErrors, ReplayDeliveryResponse, ReplayDeliveryResponses, ReplayEmailWebhooksData, ReplayEmailWebhooksError, ReplayEmailWebhooksErrors, ReplayEmailWebhooksResponse, ReplayEmailWebhooksResponses, ReplayResult, ReplyInput, ReplyToEmailData, ReplyToEmailError, ReplyToEmailErrors, ReplyToEmailResponse, ReplyToEmailResponses, ResourceId, RotateWebhookSecretData, RotateWebhookSecretError, RotateWebhookSecretErrors, RotateWebhookSecretResponse, RotateWebhookSecretResponses, SendEmailData, SendEmailError, SendEmailErrors, SendEmailResponse, SendEmailResponses, SendMailInput, SendMailResult, SendPermissionAddress, SendPermissionAnyRecipient, SendPermissionManagedZone, SendPermissionRule, SendPermissionsMeta, SendPermissionYourDomain, SentEmailDetail, SentEmailStatus, SentEmailSummary, StartCliLoginData, StartCliLoginError, StartCliLoginErrors, StartCliLoginInput, StartCliLoginResponse, StartCliLoginResponses, StorageStats, SuccessEnvelope, TestEndpointData, TestEndpointError, TestEndpointErrors, TestEndpointResponse, TestEndpointResponses, TestResult, UnverifiedDomain, UpdateAccountData, UpdateAccountError, UpdateAccountErrors, UpdateAccountInput, UpdateAccountResponse, UpdateAccountResponses, UpdateDomainData, UpdateDomainError, UpdateDomainErrors, UpdateDomainInput, UpdateDomainResponse, UpdateDomainResponses, UpdateEndpointData, UpdateEndpointError, UpdateEndpointErrors, UpdateEndpointInput, UpdateEndpointResponse, UpdateEndpointResponses, UpdateFilterData, UpdateFilterError, UpdateFilterErrors, UpdateFilterInput, UpdateFilterResponse, UpdateFilterResponses, VerifiedDomain, VerifyDomainData, VerifyDomainError, VerifyDomainErrors, VerifyDomainResponse, VerifyDomainResponses, WebhookSecret } from './types.gen.js'; diff --git a/sdk-node/src/api/generated/sdk.gen.ts b/sdk-node/src/api/generated/sdk.gen.ts index 17b557e..6f2f504 100644 --- a/sdk-node/src/api/generated/sdk.gen.ts +++ b/sdk-node/src/api/generated/sdk.gen.ts @@ -2,7 +2,7 @@ import type { Client, Options as Options2, TDataShape } from './client/index.js'; import { client } from './client.gen.js'; -import type { AddDomainData, AddDomainErrors, AddDomainResponses, CreateEndpointData, CreateEndpointErrors, CreateEndpointResponses, CreateFilterData, CreateFilterErrors, CreateFilterResponses, DeleteDomainData, DeleteDomainErrors, DeleteDomainResponses, DeleteEmailData, DeleteEmailErrors, DeleteEmailResponses, DeleteEndpointData, DeleteEndpointErrors, DeleteEndpointResponses, DeleteFilterData, DeleteFilterErrors, DeleteFilterResponses, DiscardEmailContentData, DiscardEmailContentErrors, DiscardEmailContentResponses, DownloadAttachmentsData, DownloadAttachmentsErrors, DownloadAttachmentsResponses, DownloadRawEmailData, DownloadRawEmailErrors, DownloadRawEmailResponses, GetAccountData, GetAccountErrors, GetAccountResponses, GetEmailData, GetEmailErrors, GetEmailResponses, GetSendPermissionsData, GetSendPermissionsErrors, GetSendPermissionsResponses, GetSentEmailData, GetSentEmailErrors, GetSentEmailResponses, GetStorageStatsData, GetStorageStatsErrors, GetStorageStatsResponses, GetWebhookSecretData, GetWebhookSecretErrors, GetWebhookSecretResponses, ListDeliveriesData, ListDeliveriesErrors, ListDeliveriesResponses, ListDomainsData, ListDomainsErrors, ListDomainsResponses, ListEmailsData, ListEmailsErrors, ListEmailsResponses, ListEndpointsData, ListEndpointsErrors, ListEndpointsResponses, ListFiltersData, ListFiltersErrors, ListFiltersResponses, ListSentEmailsData, ListSentEmailsErrors, ListSentEmailsResponses, ReplayDeliveryData, ReplayDeliveryErrors, ReplayDeliveryResponses, ReplayEmailWebhooksData, ReplayEmailWebhooksErrors, ReplayEmailWebhooksResponses, ReplyToEmailData, ReplyToEmailErrors, ReplyToEmailResponses, RotateWebhookSecretData, RotateWebhookSecretErrors, RotateWebhookSecretResponses, SendEmailData, SendEmailErrors, SendEmailResponses, TestEndpointData, TestEndpointErrors, TestEndpointResponses, UpdateAccountData, UpdateAccountErrors, UpdateAccountResponses, UpdateDomainData, UpdateDomainErrors, UpdateDomainResponses, UpdateEndpointData, UpdateEndpointErrors, UpdateEndpointResponses, UpdateFilterData, UpdateFilterErrors, UpdateFilterResponses, VerifyDomainData, VerifyDomainErrors, VerifyDomainResponses } from './types.gen.js'; +import type { AddDomainData, AddDomainErrors, AddDomainResponses, CliLogoutData, CliLogoutErrors, CliLogoutResponses, CreateEndpointData, CreateEndpointErrors, CreateEndpointResponses, CreateFilterData, CreateFilterErrors, CreateFilterResponses, DeleteDomainData, DeleteDomainErrors, DeleteDomainResponses, DeleteEmailData, DeleteEmailErrors, DeleteEmailResponses, DeleteEndpointData, DeleteEndpointErrors, DeleteEndpointResponses, DeleteFilterData, DeleteFilterErrors, DeleteFilterResponses, DiscardEmailContentData, DiscardEmailContentErrors, DiscardEmailContentResponses, DownloadAttachmentsData, DownloadAttachmentsErrors, DownloadAttachmentsResponses, DownloadRawEmailData, DownloadRawEmailErrors, DownloadRawEmailResponses, GetAccountData, GetAccountErrors, GetAccountResponses, GetEmailData, GetEmailErrors, GetEmailResponses, GetSendPermissionsData, GetSendPermissionsErrors, GetSendPermissionsResponses, GetSentEmailData, GetSentEmailErrors, GetSentEmailResponses, GetStorageStatsData, GetStorageStatsErrors, GetStorageStatsResponses, GetWebhookSecretData, GetWebhookSecretErrors, GetWebhookSecretResponses, ListDeliveriesData, ListDeliveriesErrors, ListDeliveriesResponses, ListDomainsData, ListDomainsErrors, ListDomainsResponses, ListEmailsData, ListEmailsErrors, ListEmailsResponses, ListEndpointsData, ListEndpointsErrors, ListEndpointsResponses, ListFiltersData, ListFiltersErrors, ListFiltersResponses, ListSentEmailsData, ListSentEmailsErrors, ListSentEmailsResponses, PollCliLoginData, PollCliLoginErrors, PollCliLoginResponses, ReplayDeliveryData, ReplayDeliveryErrors, ReplayDeliveryResponses, ReplayEmailWebhooksData, ReplayEmailWebhooksErrors, ReplayEmailWebhooksResponses, ReplyToEmailData, ReplyToEmailErrors, ReplyToEmailResponses, RotateWebhookSecretData, RotateWebhookSecretErrors, RotateWebhookSecretResponses, SendEmailData, SendEmailErrors, SendEmailResponses, StartCliLoginData, StartCliLoginErrors, StartCliLoginResponses, TestEndpointData, TestEndpointErrors, TestEndpointResponses, UpdateAccountData, UpdateAccountErrors, UpdateAccountResponses, UpdateDomainData, UpdateDomainErrors, UpdateDomainResponses, UpdateEndpointData, UpdateEndpointErrors, UpdateEndpointResponses, UpdateFilterData, UpdateFilterErrors, UpdateFilterResponses, VerifyDomainData, VerifyDomainErrors, VerifyDomainResponses } from './types.gen.js'; export type Options = Options2 & { /** @@ -18,6 +18,57 @@ export type Options; }; +/** + * Start CLI browser login + * + * Starts a browser-assisted CLI login session. The response includes a + * device code for polling and a user code that the user approves in the + * browser. This endpoint does not require an API key. + * + */ +export const startCliLogin = (options?: Options) => (options?.client ?? client).post({ + url: '/cli/login/start', + ...options, + headers: { + 'Content-Type': 'application/json', + ...options?.headers + } +}); + +/** + * Poll CLI browser login + * + * Polls a CLI login session until the browser approval either succeeds, + * is denied, expires, or is polled too quickly. The API key is generated + * only after approval and is returned exactly once. + * + */ +export const pollCliLogin = (options: Options) => (options.client ?? client).post({ + url: '/cli/login/poll', + ...options, + headers: { + 'Content-Type': 'application/json', + ...options.headers + } +}); + +/** + * Revoke the current CLI API key + * + * Revokes the API key used to authenticate the request. CLI clients use + * this endpoint during `primitive logout` before removing local credentials. + * + */ +export const cliLogout = (options?: Options) => (options?.client ?? client).post({ + security: [{ scheme: 'bearer', type: 'http' }], + url: '/cli/logout', + ...options, + headers: { + 'Content-Type': 'application/json', + ...options?.headers + } +}); + /** * Get account info */ diff --git a/sdk-node/src/api/generated/types.gen.ts b/sdk-node/src/api/generated/types.gen.ts index dc0b0bd..ecebe27 100644 --- a/sdk-node/src/api/generated/types.gen.ts +++ b/sdk-node/src/api/generated/types.gen.ts @@ -31,7 +31,7 @@ export type PaginationMeta = { export type ErrorResponse = { success: boolean; error: { - code: 'unauthorized' | 'forbidden' | 'not_found' | 'validation_error' | 'rate_limit_exceeded' | 'internal_error' | 'conflict' | 'mx_conflict' | 'outbound_disabled' | 'cannot_send_from_domain' | 'recipient_not_allowed' | 'outbound_key_missing' | 'outbound_unreachable' | 'outbound_key_invalid' | 'outbound_capacity_exhausted' | 'outbound_response_malformed' | 'outbound_relay_failed' | 'discard_not_enabled' | 'inbound_not_repliable'; + code: 'unauthorized' | 'forbidden' | 'not_found' | 'validation_error' | 'rate_limit_exceeded' | 'internal_error' | 'conflict' | 'mx_conflict' | 'outbound_disabled' | 'cannot_send_from_domain' | 'recipient_not_allowed' | 'outbound_key_missing' | 'outbound_unreachable' | 'outbound_key_invalid' | 'outbound_capacity_exhausted' | 'outbound_response_malformed' | 'outbound_relay_failed' | 'discard_not_enabled' | 'inbound_not_repliable' | 'authorization_pending' | 'slow_down' | 'access_denied' | 'expired_token' | 'invalid_device_code'; message: string; /** * Optional structured data that callers can inspect to recover @@ -117,6 +117,73 @@ export type GateFix = { subject: string; }; +export type StartCliLoginInput = { + /** + * Human-readable device name shown during browser approval + */ + device_name?: string; + /** + * Optional client metadata stored with the login session + */ + metadata?: { + [key: string]: unknown; + }; +}; + +export type CliLoginStartResult = { + /** + * Opaque code used by the CLI to poll for approval + */ + device_code: string; + /** + * Short code the user confirms in the browser + */ + user_code: string; + /** + * Browser URL where the user approves the login + */ + verification_uri: string; + /** + * Browser URL with the user code prefilled + */ + verification_uri_complete: string; + /** + * Seconds until the login session expires + */ + expires_in: number; + /** + * Minimum seconds between poll requests + */ + interval: number; +}; + +export type PollCliLoginInput = { + device_code: string; +}; + +export type CliLoginPollResult = { + /** + * Newly-created API key for CLI authentication + */ + api_key: string; + key_id: string; + key_prefix: string; + org_id: string; + org_name: string | null; +}; + +export type CliLogoutInput = { + /** + * Optional key id guard; when provided it must match the authenticated API key + */ + key_id?: string; +}; + +export type CliLogoutResult = { + revoked: boolean; + key_id: string; +}; + export type Account = { id: string; email: string; @@ -1182,6 +1249,111 @@ export type Cursor = string; */ export type Limit = number; +export type StartCliLoginData = { + body?: StartCliLoginInput; + path?: never; + query?: never; + url: '/cli/login/start'; +}; + +export type StartCliLoginErrors = { + /** + * Invalid request parameters + */ + 400: ErrorResponse; + /** + * Rate limit exceeded + */ + 429: ErrorResponse; +}; + +export type StartCliLoginError = StartCliLoginErrors[keyof StartCliLoginErrors]; + +export type StartCliLoginResponses = { + /** + * CLI login session created + */ + 201: SuccessEnvelope & { + data?: CliLoginStartResult; + }; +}; + +export type StartCliLoginResponse = StartCliLoginResponses[keyof StartCliLoginResponses]; + +export type PollCliLoginData = { + body: PollCliLoginInput; + path?: never; + query?: never; + url: '/cli/login/poll'; +}; + +export type PollCliLoginErrors = { + /** + * Invalid request, pending authorization, expired token, or invalid device code + */ + 400: ErrorResponse; + /** + * CLI login was denied in the browser + */ + 403: ErrorResponse; + /** + * Polling too quickly + */ + 429: ErrorResponse; +}; + +export type PollCliLoginError = PollCliLoginErrors[keyof PollCliLoginErrors]; + +export type PollCliLoginResponses = { + /** + * CLI login approved and API key created + */ + 200: SuccessEnvelope & { + data?: CliLoginPollResult; + }; +}; + +export type PollCliLoginResponse = PollCliLoginResponses[keyof PollCliLoginResponses]; + +export type CliLogoutData = { + body?: CliLogoutInput; + path?: never; + query?: never; + url: '/cli/logout'; +}; + +export type CliLogoutErrors = { + /** + * Invalid request parameters + */ + 400: ErrorResponse; + /** + * Invalid or missing API key + */ + 401: ErrorResponse; + /** + * Authenticated caller lacks permission for the operation + */ + 403: ErrorResponse; + /** + * Resource not found + */ + 404: ErrorResponse; +}; + +export type CliLogoutError = CliLogoutErrors[keyof CliLogoutErrors]; + +export type CliLogoutResponses = { + /** + * CLI API key revoked + */ + 200: SuccessEnvelope & { + data?: CliLogoutResult; + }; +}; + +export type CliLogoutResponse = CliLogoutResponses[keyof CliLogoutResponses]; + export type GetAccountData = { body?: never; path?: never; diff --git a/sdk-node/src/oclif/api-command.ts b/sdk-node/src/oclif/api-command.ts index b2ddbb4..f1a32a0 100644 --- a/sdk-node/src/oclif/api-command.ts +++ b/sdk-node/src/oclif/api-command.ts @@ -5,6 +5,11 @@ import type { PrimitiveOperationManifest, PrimitiveParameterManifest, } from "../openapi/index.js"; +import { + deleteCliCredentials, + type ResolvedCliAuth, + resolveCliAuth, +} from "./auth.js"; type OperationName = keyof typeof operations; @@ -374,7 +379,7 @@ export function extractErrorCode(payload: unknown): string | undefined { // special-case every command. const ERROR_CODE_HINTS: Record = { unauthorized: - "Hint: pass --api-key explicitly, or set PRIMITIVE_API_KEY in your environment. `primitive whoami` is the fastest way to verify a key is live.", + "Hint: run `primitive login`, pass --api-key explicitly, or set PRIMITIVE_API_KEY in your environment. `primitive whoami` is the fastest way to verify a key is live.", }; // Write a server / SDK error to stderr in the canonical envelope @@ -390,6 +395,38 @@ export function writeErrorWithHints(payload: unknown): void { } } +export function removeStaleSavedCredentialOnUnauthorized(params: { + auth: ResolvedCliAuth; + baseUrlOverridden: boolean; + configDir: string; + payload: unknown; +}): boolean { + if ( + extractErrorCode(params.payload) !== "unauthorized" || + params.auth.source !== "stored" + ) { + return false; + } + + const baseUrlDiffersFromSaved = + params.baseUrlOverridden && + params.auth.credentials !== null && + params.auth.baseUrl !== params.auth.credentials.base_url; + + if (baseUrlDiffersFromSaved) { + process.stderr.write( + "Saved Primitive CLI credentials were rejected by the overridden API base URL. The local credential was not removed; check --base-url / PRIMITIVE_API_URL, or run `primitive logout` to remove it.\n", + ); + return false; + } + + deleteCliCredentials(params.configDir); + process.stderr.write( + "Removed saved Primitive CLI credentials because the backing API key is no longer valid. Run `primitive login` to create a new one.\n", + ); + return true; +} + // Format milliseconds as a short human-readable wall-clock duration. // Sub-second uses 2 decimal places (e.g. `0.18s`); seconds use 2 // decimals up to 60s (`12.34s`); minute-plus uses `Mm SS.SSs`. @@ -495,7 +532,8 @@ function buildFlags(operation: PrimitiveOperationManifest): { } { const flags: Record = { "api-key": Flags.string({ - description: "Primitive API key (defaults to PRIMITIVE_API_KEY)", + description: + "Primitive API key (defaults to PRIMITIVE_API_KEY or saved `primitive login` credentials)", env: "PRIMITIVE_API_KEY", }), "base-url": Flags.string({ @@ -630,7 +668,8 @@ export function createOperationCommand( const { flags } = await this.parse(OperationCommand as never); const parsedFlags = flags as Record; await runWithTiming(parsedFlags.time === true, async () => { - const apiClient = new PrimitiveApiClient({ + const baseUrlOverridden = typeof parsedFlags["base-url"] === "string"; + const auth = resolveCliAuth({ apiKey: typeof parsedFlags["api-key"] === "string" ? (parsedFlags["api-key"] as string) @@ -639,6 +678,11 @@ export function createOperationCommand( typeof parsedFlags["base-url"] === "string" ? (parsedFlags["base-url"] as string) : undefined, + configDir: this.config.configDir, + }); + const apiClient = new PrimitiveApiClient({ + apiKey: auth.apiKey, + baseUrl: auth.baseUrl, }); // Two body sources, merged: explicit JSON via --body / @@ -707,7 +751,14 @@ export function createOperationCommand( }); if (result.error) { - writeErrorWithHints(extractErrorPayload(result.error)); + const errorPayload = extractErrorPayload(result.error); + writeErrorWithHints(errorPayload); + removeStaleSavedCredentialOnUnauthorized({ + auth, + baseUrlOverridden, + configDir: this.config.configDir, + payload: errorPayload, + }); process.exitCode = 1; return; } diff --git a/sdk-node/src/oclif/auth.ts b/sdk-node/src/oclif/auth.ts new file mode 100644 index 0000000..fad3c10 --- /dev/null +++ b/sdk-node/src/oclif/auth.ts @@ -0,0 +1,162 @@ +import { + chmodSync, + mkdirSync, + readFileSync, + rmSync, + writeFileSync, +} from "node:fs"; +import { join } from "node:path"; +import { DEFAULT_BASE_URL } from "../api/index.js"; + +const CREDENTIALS_FILE = "credentials.json"; + +export type StoredCliCredentials = { + api_key: string; + key_id: string; + key_prefix: string; + org_id: string; + org_name: string | null; + base_url: string; + created_at: string; +}; + +export type ResolvedCliAuth = { + apiKey: string | undefined; + baseUrl: string; + source: "flag-or-env" | "stored" | "none"; + credentials: StoredCliCredentials | null; +}; + +function isRecord(value: unknown): value is Record { + return value !== null && typeof value === "object" && !Array.isArray(value); +} + +function requireString( + value: Record, + key: keyof StoredCliCredentials, +): string { + const raw = value[key]; + if (typeof raw !== "string" || raw.trim().length === 0) { + throw new Error( + "Stored Primitive CLI credentials are malformed. Run `primitive logout` and then `primitive login`.", + ); + } + return raw; +} + +function parseCredentials(raw: unknown): StoredCliCredentials { + if (!isRecord(raw)) { + throw new Error( + "Stored Primitive CLI credentials are malformed. Run `primitive logout` and then `primitive login`.", + ); + } + + const orgName = raw.org_name; + if (orgName !== null && typeof orgName !== "string") { + throw new Error( + "Stored Primitive CLI credentials are malformed. Run `primitive logout` and then `primitive login`.", + ); + } + + return { + api_key: requireString(raw, "api_key"), + key_id: requireString(raw, "key_id"), + key_prefix: requireString(raw, "key_prefix"), + org_id: requireString(raw, "org_id"), + org_name: orgName, + base_url: requireString(raw, "base_url"), + created_at: requireString(raw, "created_at"), + }; +} + +export function credentialsPath(configDir: string): string { + return join(configDir, CREDENTIALS_FILE); +} + +export function normalizeBaseUrl(baseUrl: string | undefined): string { + const trimmed = baseUrl?.trim(); + if (!trimmed) return DEFAULT_BASE_URL; + return trimmed.replace(/\/+$/, ""); +} + +export function loadCliCredentials( + configDir: string, +): StoredCliCredentials | null { + const path = credentialsPath(configDir); + let contents: string; + try { + contents = readFileSync(path, "utf8"); + } catch (error) { + if ( + error && + typeof error === "object" && + (error as { code?: unknown }).code === "ENOENT" + ) { + return null; + } + const detail = error instanceof Error ? error.message : String(error); + throw new Error(`Could not read Primitive CLI credentials: ${detail}`); + } + + try { + return parseCredentials(JSON.parse(contents)); + } catch (error) { + if (error instanceof SyntaxError) { + throw new Error( + "Stored Primitive CLI credentials are not valid JSON. Run `primitive logout` and then `primitive login`.", + ); + } + throw error; + } +} + +export function saveCliCredentials( + configDir: string, + credentials: StoredCliCredentials, +): void { + mkdirSync(configDir, { mode: 0o700, recursive: true }); + const path = credentialsPath(configDir); + writeFileSync(path, `${JSON.stringify(credentials, null, 2)}\n`, { + mode: 0o600, + }); + chmodSync(path, 0o600); +} + +export function deleteCliCredentials(configDir: string): void { + rmSync(credentialsPath(configDir), { force: true }); +} + +export function resolveCliAuth(params: { + configDir: string; + apiKey?: string; + baseUrl?: string; +}): ResolvedCliAuth { + const apiKey = params.apiKey?.trim(); + if (apiKey) { + return { + apiKey, + baseUrl: normalizeBaseUrl(params.baseUrl), + credentials: null, + source: "flag-or-env", + }; + } + + const credentials = loadCliCredentials(params.configDir); + if (credentials) { + return { + apiKey: credentials.api_key, + baseUrl: params.baseUrl + ? normalizeBaseUrl(params.baseUrl) + : credentials.base_url, + credentials, + source: "stored", + }; + } + + return { + apiKey: undefined, + baseUrl: normalizeBaseUrl(params.baseUrl), + credentials: null, + source: "none", + }; +} diff --git a/sdk-node/src/oclif/commands/emails-latest.ts b/sdk-node/src/oclif/commands/emails-latest.ts index 3cbedbe..9fa588f 100644 --- a/sdk-node/src/oclif/commands/emails-latest.ts +++ b/sdk-node/src/oclif/commands/emails-latest.ts @@ -4,10 +4,12 @@ import type { EmailSummary } from "../../api/generated/types.gen.js"; import { PrimitiveApiClient } from "../../api/index.js"; import { extractErrorPayload, + removeStaleSavedCredentialOnUnauthorized, runWithTiming, TIME_FLAG_DESCRIPTION, writeErrorWithHints, } from "../api-command.js"; +import { resolveCliAuth } from "../auth.js"; // `primitive emails:latest` is the agent-grade shortcut for "show me // the most recent inbound emails as something I can read at a glance." @@ -108,7 +110,8 @@ class EmailsLatestCommand extends Command { static flags = { "api-key": Flags.string({ - description: "Primitive API key (defaults to PRIMITIVE_API_KEY)", + description: + "Primitive API key (defaults to PRIMITIVE_API_KEY or saved `primitive login` credentials)", env: "PRIMITIVE_API_KEY", }), "base-url": Flags.string({ @@ -137,9 +140,15 @@ class EmailsLatestCommand extends Command { const { flags } = await this.parse(EmailsLatestCommand); await runWithTiming(flags.time, async () => { - const apiClient = new PrimitiveApiClient({ + const baseUrlOverridden = flags["base-url"] !== undefined; + const auth = resolveCliAuth({ apiKey: flags["api-key"], baseUrl: flags["base-url"], + configDir: this.config.configDir, + }); + const apiClient = new PrimitiveApiClient({ + apiKey: auth.apiKey, + baseUrl: auth.baseUrl, }); const result = await listEmails({ @@ -149,7 +158,14 @@ class EmailsLatestCommand extends Command { }); if (result.error) { - writeErrorWithHints(extractErrorPayload(result.error)); + const errorPayload = extractErrorPayload(result.error); + writeErrorWithHints(errorPayload); + removeStaleSavedCredentialOnUnauthorized({ + auth, + baseUrlOverridden, + configDir: this.config.configDir, + payload: errorPayload, + }); process.exitCode = 1; return; } diff --git a/sdk-node/src/oclif/commands/login.ts b/sdk-node/src/oclif/commands/login.ts new file mode 100644 index 0000000..44493ca --- /dev/null +++ b/sdk-node/src/oclif/commands/login.ts @@ -0,0 +1,267 @@ +import { spawn } from "node:child_process"; +import { hostname } from "node:os"; +import { Command, Errors, Flags } from "@oclif/core"; +import { + getAccount, + pollCliLogin, + startCliLogin, +} from "../../api/generated/sdk.gen.js"; +import type { + CliLoginPollResult, + CliLoginStartResult, +} from "../../api/generated/types.gen.js"; +import { PrimitiveApiClient } from "../../api/index.js"; +import { + extractErrorCode, + extractErrorPayload, + removeStaleSavedCredentialOnUnauthorized, + writeErrorWithHints, +} from "../api-command.js"; +import { + loadCliCredentials, + normalizeBaseUrl, + type StoredCliCredentials, + saveCliCredentials, +} from "../auth.js"; + +function cliError(message: string): Errors.CLIError { + return new Errors.CLIError(message, { exit: 1 }); +} + +function sleep(ms: number): Promise { + return new Promise((resolve) => setTimeout(resolve, ms)); +} + +function openBrowser(url: string): void { + const command = + process.platform === "darwin" + ? "open" + : process.platform === "win32" + ? "cmd" + : "xdg-open"; + const args = process.platform === "win32" ? ["/c", "start", "", url] : [url]; + const child = spawn(command, args, { detached: true, stdio: "ignore" }); + child.on("error", () => undefined); + child.unref(); +} + +function unwrapData(value: unknown): T | null { + const envelope = value as { data?: T } | null | undefined; + return envelope?.data ?? null; +} + +function retryAfterSeconds(result: unknown): number | null { + const response = (result as { response?: Response }).response; + const raw = response?.headers.get("retry-after"); + if (!raw) return null; + const parsed = Number.parseInt(raw, 10); + return Number.isFinite(parsed) && parsed > 0 ? parsed : null; +} + +type ExistingLoginStatus = + | { status: "valid" } + | { status: "removed_stale" } + | { status: "blocked"; message: string; payload: unknown }; + +export async function checkExistingLogin(params: { + baseUrl?: string; + configDir: string; + credentials: StoredCliCredentials; + checkAccount?: ( + apiClient: PrimitiveApiClient, + ) => Promise<{ error?: unknown }>; +}): Promise { + const baseUrlOverridden = params.baseUrl !== undefined; + const probeBaseUrl = baseUrlOverridden + ? normalizeBaseUrl(params.baseUrl) + : params.credentials.base_url; + const apiClient = new PrimitiveApiClient({ + apiKey: params.credentials.api_key, + baseUrl: probeBaseUrl, + }); + const result = await ( + params.checkAccount ?? + ((client) => + getAccount({ + client: client.client, + responseStyle: "fields", + })) + )(apiClient); + + if (!result.error) return { status: "valid" }; + + const payload = extractErrorPayload(result.error); + const auth = { + apiKey: params.credentials.api_key, + baseUrl: probeBaseUrl, + credentials: params.credentials, + source: "stored" as const, + }; + const removed = removeStaleSavedCredentialOnUnauthorized({ + auth, + baseUrlOverridden, + configDir: params.configDir, + payload, + }); + if (removed) return { status: "removed_stale" }; + + const code = extractErrorCode(payload); + return { + status: "blocked", + payload, + message: + code === "unauthorized" + ? "Saved Primitive CLI credentials were rejected. Run `primitive logout` to remove them before logging in again." + : "A saved Primitive CLI login exists, but the CLI could not verify whether it is still valid. Run `primitive logout` before logging in again.", + }; +} + +class LoginCommand extends Command { + static description = + "Log in by opening Primitive in your browser and saving an org-scoped CLI API key locally."; + + static summary = "Log in with browser approval"; + + static examples = [ + "<%= config.bin %> login", + "<%= config.bin %> login --device-name work-laptop", + ]; + + static flags = { + "base-url": Flags.string({ + description: "API base URL (defaults to PRIMITIVE_API_URL or production)", + env: "PRIMITIVE_API_URL", + }), + "device-name": Flags.string({ + description: "Device name shown in the browser approval screen", + }), + "no-browser": Flags.boolean({ + description: "Do not attempt to open the browser automatically", + }), + }; + + async run(): Promise { + const { flags } = await this.parse(LoginCommand); + const baseUrl = normalizeBaseUrl(flags["base-url"]); + const existing = loadCliCredentials(this.config.configDir); + if (existing) { + const existingStatus = await checkExistingLogin({ + baseUrl: flags["base-url"], + configDir: this.config.configDir, + credentials: existing, + }); + if (existingStatus.status === "removed_stale") { + process.stderr.write("Continuing with a new Primitive CLI login...\n"); + } else if (existingStatus.status === "blocked") { + writeErrorWithHints(existingStatus.payload); + throw cliError(existingStatus.message); + } else { + const org = existing.org_name ? ` for ${existing.org_name}` : ""; + throw cliError( + `Already logged in${org}. Run \`primitive logout\` before logging in again.`, + ); + } + } + + const apiClient = new PrimitiveApiClient({ baseUrl }); + const deviceName = flags["device-name"] ?? hostname(); + const started = await startCliLogin({ + body: { + device_name: deviceName, + metadata: { + arch: process.arch, + platform: process.platform, + version: this.config.version, + }, + }, + client: apiClient.client, + responseStyle: "fields", + }); + + if (started.error) { + writeErrorWithHints(extractErrorPayload(started.error)); + throw cliError("Could not start Primitive CLI login."); + } + + const start = unwrapData(started.data); + if (!start) { + throw cliError("Primitive API returned an empty CLI login response."); + } + + process.stderr.write(`Your login code is: ${start.user_code}\n`); + if (!flags["no-browser"]) { + try { + openBrowser(start.verification_uri_complete); + process.stderr.write("Opening Primitive in your browser...\n"); + } catch { + process.stderr.write("Could not open a browser automatically.\n"); + } + } + process.stderr.write( + `If the browser did not open, visit: ${start.verification_uri_complete}\n`, + ); + process.stderr.write("Waiting for browser approval...\n"); + + const deadline = Date.now() + start.expires_in * 1000; + let interval = start.interval; + + while (Date.now() < deadline) { + await sleep(interval * 1000); + + const polled = await pollCliLogin({ + body: { device_code: start.device_code }, + client: apiClient.client, + responseStyle: "fields", + }); + + if (polled.data) { + const login = unwrapData(polled.data); + if (!login) { + throw cliError("Primitive API returned an empty CLI poll response."); + } + + saveCliCredentials(this.config.configDir, { + api_key: login.api_key, + base_url: baseUrl, + created_at: new Date().toISOString(), + key_id: login.key_id, + key_prefix: login.key_prefix, + org_id: login.org_id, + org_name: login.org_name, + }); + + const org = login.org_name ? ` (${login.org_name})` : ""; + process.stderr.write(`Logged in to org ${login.org_id}${org}.\n`); + return; + } + + const payload = extractErrorPayload(polled.error); + const code = extractErrorCode(payload); + if (code === "authorization_pending") continue; + if (code === "slow_down") { + interval = retryAfterSeconds(polled) ?? interval + 5; + continue; + } + if (code === "access_denied") { + throw cliError("Primitive CLI login was denied in the browser."); + } + if (code === "expired_token") { + throw cliError( + "Primitive CLI login expired. Run `primitive login` again.", + ); + } + if (code === "invalid_device_code") { + throw cliError( + "Primitive CLI login device code is invalid. Run `primitive login` again.", + ); + } + + writeErrorWithHints(payload); + throw cliError("Primitive CLI login failed while polling for approval."); + } + + throw cliError("Primitive CLI login expired. Run `primitive login` again."); + } +} + +export default LoginCommand; diff --git a/sdk-node/src/oclif/commands/logout.ts b/sdk-node/src/oclif/commands/logout.ts new file mode 100644 index 0000000..9fcc234 --- /dev/null +++ b/sdk-node/src/oclif/commands/logout.ts @@ -0,0 +1,99 @@ +import { Command, Errors, Flags } from "@oclif/core"; +import { cliLogout } from "../../api/generated/sdk.gen.js"; +import type { CliLogoutResult } from "../../api/generated/types.gen.js"; +import { PrimitiveApiClient } from "../../api/index.js"; +import { + extractErrorCode, + extractErrorPayload, + writeErrorWithHints, +} from "../api-command.js"; +import { + deleteCliCredentials, + loadCliCredentials, + normalizeBaseUrl, +} from "../auth.js"; + +function cliError(message: string): Errors.CLIError { + return new Errors.CLIError(message, { exit: 1 }); +} + +function unwrapData(value: unknown): T | null { + const envelope = value as { data?: T } | null | undefined; + return envelope?.data ?? null; +} + +class LogoutCommand extends Command { + static description = + "Log out by revoking the saved Primitive CLI API key and deleting local credentials."; + + static summary = "Log out and revoke the saved CLI key"; + + static examples = ["<%= config.bin %> logout"]; + + static flags = { + "base-url": Flags.string({ + description: "Override the API base URL used for key revocation", + env: "PRIMITIVE_API_URL", + }), + }; + + async run(): Promise { + const { flags } = await this.parse(LogoutCommand); + let credentials: ReturnType; + try { + credentials = loadCliCredentials(this.config.configDir); + } catch (error) { + deleteCliCredentials(this.config.configDir); + const detail = error instanceof Error ? error.message : String(error); + process.stderr.write( + `Removed unreadable Primitive CLI credentials. Backing API key was not revoked: ${detail}\n`, + ); + process.exitCode = 1; + return; + } + if (!credentials) { + throw cliError( + "Not logged in. Run `primitive login` to create saved CLI credentials.", + ); + } + + const baseUrl = flags["base-url"] + ? normalizeBaseUrl(flags["base-url"]) + : credentials.base_url; + const apiClient = new PrimitiveApiClient({ + apiKey: credentials.api_key, + baseUrl, + }); + + const result = await cliLogout({ + body: { key_id: credentials.key_id }, + client: apiClient.client, + responseStyle: "fields", + }); + + if (result.error) { + const payload = extractErrorPayload(result.error); + const code = extractErrorCode(payload); + if (code === "unauthorized" || code === "not_found") { + deleteCliCredentials(this.config.configDir); + writeErrorWithHints(payload); + process.stderr.write( + "Removed saved Primitive CLI credentials because the backing API key is already unavailable.\n", + ); + process.exitCode = 1; + return; + } + + writeErrorWithHints(payload); + throw cliError("Could not revoke the saved Primitive CLI API key."); + } + + const logout = unwrapData(result.data); + deleteCliCredentials(this.config.configDir); + + const keyId = logout?.key_id ?? credentials.key_id; + process.stderr.write(`Logged out and revoked API key ${keyId}.\n`); + } +} + +export default LogoutCommand; diff --git a/sdk-node/src/oclif/commands/send.ts b/sdk-node/src/oclif/commands/send.ts index af7956e..c7900bd 100644 --- a/sdk-node/src/oclif/commands/send.ts +++ b/sdk-node/src/oclif/commands/send.ts @@ -11,10 +11,12 @@ import { extractErrorCode, extractErrorPayload, formatErrorPayload, + removeStaleSavedCredentialOnUnauthorized, runWithTiming, TIME_FLAG_DESCRIPTION, writeErrorWithHints, } from "../api-command.js"; +import { type ResolvedCliAuth, resolveCliAuth } from "../auth.js"; // `primitive send` is the agent-grade shortcut for the most common // case: send a fresh outbound email. It wraps `sending:send-email` @@ -72,6 +74,11 @@ function isVerifiedDomain(domain: Domain): domain is VerifiedDomain { async function pickDefaultFromAddress( apiClient: PrimitiveApiClient, + authFailureContext: { + auth: ResolvedCliAuth; + baseUrlOverridden: boolean; + configDir: string; + }, ): Promise { const result = await listDomains({ client: apiClient.client, @@ -86,6 +93,10 @@ async function pickDefaultFromAddress( // wrapping. if (extractErrorCode(errorPayload) === "unauthorized") { writeErrorWithHints(errorPayload); + removeStaleSavedCredentialOnUnauthorized({ + ...authFailureContext, + payload: errorPayload, + }); // exit: 1 to match the run() unauthorized path (which uses // `process.exitCode = 1`). oclif's CLIError defaults to 2, // so without this override the same "unauthorized" condition @@ -138,7 +149,8 @@ class SendCommand extends Command { static flags = { "api-key": Flags.string({ - description: "Primitive API key (defaults to PRIMITIVE_API_KEY)", + description: + "Primitive API key (defaults to PRIMITIVE_API_KEY or saved `primitive login` credentials)", env: "PRIMITIVE_API_KEY", }), "base-url": Flags.string({ @@ -192,12 +204,25 @@ class SendCommand extends Command { } await runWithTiming(flags.time, async () => { - const apiClient = new PrimitiveApiClient({ + const baseUrlOverridden = flags["base-url"] !== undefined; + const auth = resolveCliAuth({ apiKey: flags["api-key"], baseUrl: flags["base-url"], + configDir: this.config.configDir, + }); + const apiClient = new PrimitiveApiClient({ + apiKey: auth.apiKey, + baseUrl: auth.baseUrl, }); - const from = flags.from ?? (await pickDefaultFromAddress(apiClient)); + const authFailureContext = { + auth, + baseUrlOverridden, + configDir: this.config.configDir, + }; + const from = + flags.from ?? + (await pickDefaultFromAddress(apiClient, authFailureContext)); const subject = flags.subject ?? (flags.body ? deriveSubject(flags.body) : "Message"); @@ -221,7 +246,12 @@ class SendCommand extends Command { }); if (result.error) { - writeErrorWithHints(extractErrorPayload(result.error)); + const errorPayload = extractErrorPayload(result.error); + writeErrorWithHints(errorPayload); + removeStaleSavedCredentialOnUnauthorized({ + ...authFailureContext, + payload: errorPayload, + }); process.exitCode = 1; return; } diff --git a/sdk-node/src/oclif/commands/whoami.ts b/sdk-node/src/oclif/commands/whoami.ts index ca7f92e..d8b2961 100644 --- a/sdk-node/src/oclif/commands/whoami.ts +++ b/sdk-node/src/oclif/commands/whoami.ts @@ -4,10 +4,12 @@ import type { Account } from "../../api/generated/types.gen.js"; import { PrimitiveApiClient } from "../../api/index.js"; import { extractErrorPayload, + removeStaleSavedCredentialOnUnauthorized, runWithTiming, TIME_FLAG_DESCRIPTION, writeErrorWithHints, } from "../api-command.js"; +import { resolveCliAuth } from "../auth.js"; // `primitive whoami` is the credentials smoke-test the AGX // walkthrough kept asking for. Before this command, a user with a @@ -33,7 +35,8 @@ class WhoamiCommand extends Command { static flags = { "api-key": Flags.string({ - description: "Primitive API key (defaults to PRIMITIVE_API_KEY)", + description: + "Primitive API key (defaults to PRIMITIVE_API_KEY or saved `primitive login` credentials)", env: "PRIMITIVE_API_KEY", }), "base-url": Flags.string({ @@ -49,9 +52,15 @@ class WhoamiCommand extends Command { const { flags } = await this.parse(WhoamiCommand); await runWithTiming(flags.time, async () => { - const apiClient = new PrimitiveApiClient({ + const baseUrlOverridden = flags["base-url"] !== undefined; + const auth = resolveCliAuth({ apiKey: flags["api-key"], baseUrl: flags["base-url"], + configDir: this.config.configDir, + }); + const apiClient = new PrimitiveApiClient({ + apiKey: auth.apiKey, + baseUrl: auth.baseUrl, }); const result = await getAccount({ @@ -60,7 +69,14 @@ class WhoamiCommand extends Command { }); if (result.error) { - writeErrorWithHints(extractErrorPayload(result.error)); + const errorPayload = extractErrorPayload(result.error); + writeErrorWithHints(errorPayload); + removeStaleSavedCredentialOnUnauthorized({ + auth, + baseUrlOverridden, + configDir: this.config.configDir, + payload: errorPayload, + }); process.exitCode = 1; return; } diff --git a/sdk-node/src/oclif/fish-completion.ts b/sdk-node/src/oclif/fish-completion.ts index 0a83abd..5a6799a 100644 --- a/sdk-node/src/oclif/fish-completion.ts +++ b/sdk-node/src/oclif/fish-completion.ts @@ -106,7 +106,7 @@ export function renderFishCompletion(binName: string): string { } lines.push( - `complete -c ${binName} -n '${operationCondition(operation).replace(BIN_PLACEHOLDER, binName)}' -l 'api-key' -r -d 'Primitive API key (defaults to PRIMITIVE_API_KEY)'`, + `complete -c ${binName} -n '${operationCondition(operation).replace(BIN_PLACEHOLDER, binName)}' -l 'api-key' -r -d 'Primitive API key (defaults to PRIMITIVE_API_KEY or saved primitive login credentials)'`, `complete -c ${binName} -n '${operationCondition(operation).replace(BIN_PLACEHOLDER, binName)}' -l 'base-url' -r -d 'API base URL (defaults to PRIMITIVE_API_URL or production)'`, ); diff --git a/sdk-node/src/oclif/index.ts b/sdk-node/src/oclif/index.ts index 344a951..0294fee 100644 --- a/sdk-node/src/oclif/index.ts +++ b/sdk-node/src/oclif/index.ts @@ -5,6 +5,8 @@ import { } from "../openapi/index.js"; import { createOperationCommand } from "./api-command.js"; import EmailsLatestCommand from "./commands/emails-latest.js"; +import LoginCommand from "./commands/login.js"; +import LogoutCommand from "./commands/logout.js"; import SendCommand from "./commands/send.js"; import WhoamiCommand from "./commands/whoami.js"; import { renderFishCompletion } from "./fish-completion.js"; @@ -161,6 +163,10 @@ export const COMMANDS: Record = { // operation stays available under sending:send-email for callers // who want every flag. send: SendCommand, + // `login` creates and stores an org-scoped CLI API key via browser approval. + login: LoginCommand, + // `logout` revokes the saved CLI API key and removes local credentials. + logout: LogoutCommand, // `whoami` is the credentials smoke test. Prints the account the // current API key authenticates as. AGX walkthroughs kept // wanting this before risking a real call against a possibly- diff --git a/sdk-node/src/openapi/openapi.generated.ts b/sdk-node/src/openapi/openapi.generated.ts index 1a74235..bcf4277 100644 --- a/sdk-node/src/openapi/openapi.generated.ts +++ b/sdk-node/src/openapi/openapi.generated.ts @@ -32,6 +32,10 @@ export const openapiDocument: Record = { } ], "tags": [ + { + "name": "CLI", + "description": "Browser-assisted CLI authentication" + }, { "name": "Account", "description": "Manage your account settings, storage, and webhook secret" @@ -62,6 +66,258 @@ export const openapiDocument: Record = { } ], "paths": { + "/cli/login/start": { + "post": { + "operationId": "startCliLogin", + "summary": "Start CLI browser login", + "description": "Starts a browser-assisted CLI login session. The response includes a\ndevice code for polling and a user code that the user approves in the\nbrowser. This endpoint does not require an API key.\n", + "tags": [ + "CLI" + ], + "security": [], + "requestBody": { + "required": false, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StartCliLoginInput" + } + } + } + }, + "responses": { + "201": { + "description": "CLI login session created", + "headers": { + "Cache-Control": { + "schema": { + "type": "string" + }, + "description": "Always `no-store`" + } + }, + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/SuccessEnvelope" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/CliLoginStartResult" + } + } + } + ] + } + } + } + }, + "400": { + "$ref": "#/components/responses/ValidationError" + }, + "429": { + "$ref": "#/components/responses/RateLimited" + } + } + } + }, + "/cli/login/poll": { + "post": { + "operationId": "pollCliLogin", + "summary": "Poll CLI browser login", + "description": "Polls a CLI login session until the browser approval either succeeds,\nis denied, expires, or is polled too quickly. The API key is generated\nonly after approval and is returned exactly once.\n", + "tags": [ + "CLI" + ], + "security": [], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PollCliLoginInput" + } + } + } + }, + "responses": { + "200": { + "description": "CLI login approved and API key created", + "headers": { + "Cache-Control": { + "schema": { + "type": "string" + }, + "description": "Always `no-store`" + } + }, + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/SuccessEnvelope" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/CliLoginPollResult" + } + } + } + ] + } + } + } + }, + "400": { + "description": "Invalid request, pending authorization, expired token, or invalid device code", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + }, + "examples": { + "authorization_pending": { + "summary": "Awaiting browser approval", + "value": { + "success": false, + "error": { + "code": "authorization_pending", + "message": "CLI login is still pending browser approval" + } + } + }, + "expired_token": { + "summary": "Login session expired", + "value": { + "success": false, + "error": { + "code": "expired_token", + "message": "CLI login code expired; run primitive login again" + } + } + }, + "invalid_device_code": { + "summary": "Unknown device code", + "value": { + "success": false, + "error": { + "code": "invalid_device_code", + "message": "Invalid CLI login device code" + } + } + } + } + } + } + }, + "403": { + "description": "CLI login was denied in the browser", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + }, + "example": { + "success": false, + "error": { + "code": "access_denied", + "message": "CLI login was denied in the browser" + } + } + } + } + }, + "429": { + "description": "Polling too quickly", + "headers": { + "Retry-After": { + "schema": { + "type": "integer" + }, + "description": "Seconds to wait before polling again" + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + }, + "example": { + "success": false, + "error": { + "code": "slow_down", + "message": "Polling too quickly; slow down and retry later" + } + } + } + } + } + } + } + }, + "/cli/logout": { + "post": { + "operationId": "cliLogout", + "summary": "Revoke the current CLI API key", + "description": "Revokes the API key used to authenticate the request. CLI clients use\nthis endpoint during `primitive logout` before removing local credentials.\n", + "tags": [ + "CLI" + ], + "requestBody": { + "required": false, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CliLogoutInput" + } + } + } + }, + "responses": { + "200": { + "description": "CLI API key revoked", + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/SuccessEnvelope" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/CliLogoutResult" + } + } + } + ] + } + } + } + }, + "400": { + "$ref": "#/components/responses/ValidationError" + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + }, + "403": { + "$ref": "#/components/responses/Forbidden" + }, + "404": { + "$ref": "#/components/responses/NotFound" + } + } + } + }, "/account": { "get": { "operationId": "getAccount", @@ -2158,7 +2414,12 @@ export const openapiDocument: Record = { "outbound_response_malformed", "outbound_relay_failed", "discard_not_enabled", - "inbound_not_repliable" + "inbound_not_repliable", + "authorization_pending", + "slow_down", + "access_denied", + "expired_token", + "invalid_device_code" ] }, "message": { @@ -2296,6 +2557,135 @@ export const openapiDocument: Record = { "subject" ] }, + "StartCliLoginInput": { + "type": "object", + "additionalProperties": false, + "properties": { + "device_name": { + "type": "string", + "minLength": 1, + "maxLength": 80, + "description": "Human-readable device name shown during browser approval" + }, + "metadata": { + "type": "object", + "additionalProperties": true, + "description": "Optional client metadata stored with the login session" + } + } + }, + "CliLoginStartResult": { + "type": "object", + "properties": { + "device_code": { + "type": "string", + "description": "Opaque code used by the CLI to poll for approval" + }, + "user_code": { + "type": "string", + "pattern": "^[BCDFGHJKLMNPQRSTVWXZ]{4}-[BCDFGHJKLMNPQRSTVWXZ]{4}$", + "description": "Short code the user confirms in the browser" + }, + "verification_uri": { + "type": "string", + "description": "Browser URL where the user approves the login" + }, + "verification_uri_complete": { + "type": "string", + "description": "Browser URL with the user code prefilled" + }, + "expires_in": { + "type": "integer", + "description": "Seconds until the login session expires" + }, + "interval": { + "type": "integer", + "description": "Minimum seconds between poll requests" + } + }, + "required": [ + "device_code", + "user_code", + "verification_uri", + "verification_uri_complete", + "expires_in", + "interval" + ] + }, + "PollCliLoginInput": { + "type": "object", + "additionalProperties": false, + "properties": { + "device_code": { + "type": "string", + "minLength": 1 + } + }, + "required": [ + "device_code" + ] + }, + "CliLoginPollResult": { + "type": "object", + "properties": { + "api_key": { + "type": "string", + "description": "Newly-created API key for CLI authentication" + }, + "key_id": { + "type": "string", + "format": "uuid" + }, + "key_prefix": { + "type": "string" + }, + "org_id": { + "type": "string", + "format": "uuid" + }, + "org_name": { + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "api_key", + "key_id", + "key_prefix", + "org_id", + "org_name" + ] + }, + "CliLogoutInput": { + "type": "object", + "additionalProperties": false, + "properties": { + "key_id": { + "type": "string", + "format": "uuid", + "description": "Optional key id guard; when provided it must match the authenticated API key" + } + } + }, + "CliLogoutResult": { + "type": "object", + "properties": { + "revoked": { + "type": "boolean", + "const": true + }, + "key_id": { + "type": "string", + "format": "uuid" + } + }, + "required": [ + "revoked", + "key_id" + ] + }, "Account": { "type": "object", "properties": { diff --git a/sdk-node/src/openapi/operations.generated.ts b/sdk-node/src/openapi/operations.generated.ts index 45ea8bd..ad48289 100644 --- a/sdk-node/src/openapi/operations.generated.ts +++ b/sdk-node/src/openapi/operations.generated.ts @@ -311,6 +311,183 @@ export const operationManifest: PrimitiveOperationManifest[] = [ "tag": "Account", "tagCommand": "account" }, + { + "binaryResponse": false, + "bodyRequired": false, + "command": "cli-logout", + "description": "Revokes the API key used to authenticate the request. CLI clients use\nthis endpoint during `primitive logout` before removing local credentials.\n", + "hasJsonBody": true, + "method": "POST", + "operationId": "cliLogout", + "path": "/cli/logout", + "pathParams": [], + "queryParams": [], + "requestSchema": { + "type": "object", + "additionalProperties": false, + "properties": { + "key_id": { + "type": "string", + "format": "uuid", + "description": "Optional key id guard; when provided it must match the authenticated API key" + } + } + }, + "responseSchema": { + "type": "object", + "properties": { + "revoked": { + "type": "boolean", + "const": true + }, + "key_id": { + "type": "string", + "format": "uuid" + } + }, + "required": [ + "revoked", + "key_id" + ] + }, + "sdkName": "cliLogout", + "summary": "Revoke the current CLI API key", + "tag": "CLI", + "tagCommand": "cli" + }, + { + "binaryResponse": false, + "bodyRequired": true, + "command": "poll-cli-login", + "description": "Polls a CLI login session until the browser approval either succeeds,\nis denied, expires, or is polled too quickly. The API key is generated\nonly after approval and is returned exactly once.\n", + "hasJsonBody": true, + "method": "POST", + "operationId": "pollCliLogin", + "path": "/cli/login/poll", + "pathParams": [], + "queryParams": [], + "requestSchema": { + "type": "object", + "additionalProperties": false, + "properties": { + "device_code": { + "type": "string", + "minLength": 1 + } + }, + "required": [ + "device_code" + ] + }, + "responseSchema": { + "type": "object", + "properties": { + "api_key": { + "type": "string", + "description": "Newly-created API key for CLI authentication" + }, + "key_id": { + "type": "string", + "format": "uuid" + }, + "key_prefix": { + "type": "string" + }, + "org_id": { + "type": "string", + "format": "uuid" + }, + "org_name": { + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "api_key", + "key_id", + "key_prefix", + "org_id", + "org_name" + ] + }, + "sdkName": "pollCliLogin", + "summary": "Poll CLI browser login", + "tag": "CLI", + "tagCommand": "cli" + }, + { + "binaryResponse": false, + "bodyRequired": false, + "command": "start-cli-login", + "description": "Starts a browser-assisted CLI login session. The response includes a\ndevice code for polling and a user code that the user approves in the\nbrowser. This endpoint does not require an API key.\n", + "hasJsonBody": true, + "method": "POST", + "operationId": "startCliLogin", + "path": "/cli/login/start", + "pathParams": [], + "queryParams": [], + "requestSchema": { + "type": "object", + "additionalProperties": false, + "properties": { + "device_name": { + "type": "string", + "minLength": 1, + "maxLength": 80, + "description": "Human-readable device name shown during browser approval" + }, + "metadata": { + "type": "object", + "additionalProperties": true, + "description": "Optional client metadata stored with the login session" + } + } + }, + "responseSchema": { + "type": "object", + "properties": { + "device_code": { + "type": "string", + "description": "Opaque code used by the CLI to poll for approval" + }, + "user_code": { + "type": "string", + "pattern": "^[BCDFGHJKLMNPQRSTVWXZ]{4}-[BCDFGHJKLMNPQRSTVWXZ]{4}$", + "description": "Short code the user confirms in the browser" + }, + "verification_uri": { + "type": "string", + "description": "Browser URL where the user approves the login" + }, + "verification_uri_complete": { + "type": "string", + "description": "Browser URL with the user code prefilled" + }, + "expires_in": { + "type": "integer", + "description": "Seconds until the login session expires" + }, + "interval": { + "type": "integer", + "description": "Minimum seconds between poll requests" + } + }, + "required": [ + "device_code", + "user_code", + "verification_uri", + "verification_uri_complete", + "expires_in", + "interval" + ] + }, + "sdkName": "startCliLogin", + "summary": "Start CLI browser login", + "tag": "CLI", + "tagCommand": "cli" + }, { "binaryResponse": false, "bodyRequired": true, diff --git a/sdk-node/tests/oclif/api-command.test.ts b/sdk-node/tests/oclif/api-command.test.ts index 4d96e29..188863e 100644 --- a/sdk-node/tests/oclif/api-command.test.ts +++ b/sdk-node/tests/oclif/api-command.test.ts @@ -10,9 +10,15 @@ import { formatElapsed, formatErrorPayload, readJsonBody, + removeStaleSavedCredentialOnUnauthorized, runWithTiming, writeErrorWithHints, } from "../../src/oclif/api-command.js"; +import { + loadCliCredentials, + type StoredCliCredentials, + saveCliCredentials, +} from "../../src/oclif/auth.js"; describe("formatErrorPayload", () => { it("wraps a fetch TypeError into a code/message payload instead of {}", () => { @@ -309,6 +315,100 @@ describe("writeErrorWithHints", () => { }); }); +describe("removeStaleSavedCredentialOnUnauthorized", () => { + const credentials: StoredCliCredentials = { + api_key: "prim_stale", + base_url: "https://www.primitive.dev/api/v1", + created_at: "2026-05-05T00:00:00.000Z", + key_id: "11111111-1111-4111-8111-111111111111", + key_prefix: "prim_sta...", + org_id: "22222222-2222-4222-8222-222222222222", + org_name: "Acme", + }; + + let tempDir: string; + let writes: string[]; + let writeSpy: ReturnType; + + beforeEach(() => { + tempDir = mkdtempSync(join(tmpdir(), "primitive-cli-stale-auth-test-")); + writes = []; + writeSpy = vi + .spyOn(process.stderr, "write") + .mockImplementation((chunk: unknown) => { + writes.push(typeof chunk === "string" ? chunk : String(chunk)); + return true; + }); + }); + + afterEach(() => { + writeSpy.mockRestore(); + rmSync(tempDir, { force: true, recursive: true }); + }); + + it("removes stale saved credentials when the saved key is unauthorized", () => { + saveCliCredentials(tempDir, credentials); + + const removed = removeStaleSavedCredentialOnUnauthorized({ + auth: { + apiKey: credentials.api_key, + baseUrl: credentials.base_url, + credentials, + source: "stored", + }, + baseUrlOverridden: false, + configDir: tempDir, + payload: { code: "unauthorized", message: "Invalid API key" }, + }); + + expect(removed).toBe(true); + expect(loadCliCredentials(tempDir)).toBeNull(); + expect(writes.join("")).toContain( + "Removed saved Primitive CLI credentials", + ); + }); + + it("keeps saved credentials when an overridden base URL rejects them", () => { + saveCliCredentials(tempDir, credentials); + + const removed = removeStaleSavedCredentialOnUnauthorized({ + auth: { + apiKey: credentials.api_key, + baseUrl: "http://localhost:3000/api/v1", + credentials, + source: "stored", + }, + baseUrlOverridden: true, + configDir: tempDir, + payload: { code: "unauthorized", message: "Invalid API key" }, + }); + + expect(removed).toBe(false); + expect(loadCliCredentials(tempDir)).toEqual(credentials); + expect(writes.join("")).toContain("local credential was not removed"); + }); + + it("ignores non-auth errors", () => { + saveCliCredentials(tempDir, credentials); + + const removed = removeStaleSavedCredentialOnUnauthorized({ + auth: { + apiKey: credentials.api_key, + baseUrl: credentials.base_url, + credentials, + source: "stored", + }, + baseUrlOverridden: false, + configDir: tempDir, + payload: { code: "validation_error", message: "Bad request" }, + }); + + expect(removed).toBe(false); + expect(loadCliCredentials(tempDir)).toEqual(credentials); + expect(writes).toEqual([]); + }); +}); + describe("flagForParameter", () => { it("returns a string flag without options when no enum is set", () => { const flag = flagForParameter({ diff --git a/sdk-node/tests/oclif/auth.test.ts b/sdk-node/tests/oclif/auth.test.ts new file mode 100644 index 0000000..8c83677 --- /dev/null +++ b/sdk-node/tests/oclif/auth.test.ts @@ -0,0 +1,95 @@ +import { mkdtempSync, rmSync, statSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { afterEach, beforeEach, describe, expect, it } from "vitest"; +import { + credentialsPath, + deleteCliCredentials, + loadCliCredentials, + normalizeBaseUrl, + resolveCliAuth, + type StoredCliCredentials, + saveCliCredentials, +} from "../../src/oclif/auth.js"; + +const CREDENTIALS: StoredCliCredentials = { + api_key: "prim_test", + base_url: "https://api.example.test/api/v1", + created_at: "2026-05-05T00:00:00.000Z", + key_id: "11111111-1111-4111-8111-111111111111", + key_prefix: "prim_abc", + org_id: "22222222-2222-4222-8222-222222222222", + org_name: "Acme", +}; + +describe("CLI auth credentials", () => { + let tempDir: string; + + beforeEach(() => { + tempDir = mkdtempSync(join(tmpdir(), "primitive-cli-auth-test-")); + }); + + afterEach(() => { + rmSync(tempDir, { force: true, recursive: true }); + }); + + it("returns null when no saved credentials exist", () => { + expect(loadCliCredentials(tempDir)).toBeNull(); + }); + + it("saves and loads credentials with private file permissions", () => { + saveCliCredentials(tempDir, CREDENTIALS); + + expect(loadCliCredentials(tempDir)).toEqual(CREDENTIALS); + if (process.platform !== "win32") { + expect(statSync(credentialsPath(tempDir)).mode & 0o777).toBe(0o600); + } + }); + + it("deletes saved credentials", () => { + saveCliCredentials(tempDir, CREDENTIALS); + deleteCliCredentials(tempDir); + + expect(loadCliCredentials(tempDir)).toBeNull(); + }); + + it("normalizes explicit base URLs", () => { + expect(normalizeBaseUrl("https://api.example.test/api/v1///")).toBe( + "https://api.example.test/api/v1", + ); + }); + + it("prefers explicit API keys over saved credentials", () => { + saveCliCredentials(tempDir, CREDENTIALS); + + expect( + resolveCliAuth({ + apiKey: "prim_explicit", + baseUrl: "https://override.example/api/v1", + configDir: tempDir, + }), + ).toMatchObject({ + apiKey: "prim_explicit", + baseUrl: "https://override.example/api/v1", + source: "flag-or-env", + }); + }); + + it("falls back to saved credentials and saved base URL", () => { + saveCliCredentials(tempDir, CREDENTIALS); + + expect(resolveCliAuth({ configDir: tempDir })).toMatchObject({ + apiKey: CREDENTIALS.api_key, + baseUrl: CREDENTIALS.base_url, + source: "stored", + }); + }); + + it("throws a helpful error for invalid credential JSON", () => { + writeFileSync(credentialsPath(tempDir), "not json"); + + expect(() => loadCliCredentials(tempDir)).toThrow( + /credentials are not valid JSON/, + ); + }); +}); diff --git a/sdk-node/tests/oclif/describe-command.test.ts b/sdk-node/tests/oclif/describe-command.test.ts index e7eedc1..ff8571d 100644 --- a/sdk-node/tests/oclif/describe-command.test.ts +++ b/sdk-node/tests/oclif/describe-command.test.ts @@ -5,6 +5,11 @@ describe("describe command", () => { it("registers `describe` in the COMMANDS map", () => { expect(COMMANDS.describe).toBeDefined(); }); + + it("registers browser login commands in the COMMANDS map", () => { + expect(COMMANDS.login).toBeDefined(); + expect(COMMANDS.logout).toBeDefined(); + }); }); describe("lookupOperation", () => { diff --git a/sdk-node/tests/oclif/login.test.ts b/sdk-node/tests/oclif/login.test.ts new file mode 100644 index 0000000..e61099e --- /dev/null +++ b/sdk-node/tests/oclif/login.test.ts @@ -0,0 +1,110 @@ +import { mkdtempSync, rmSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { + loadCliCredentials, + type StoredCliCredentials, + saveCliCredentials, +} from "../../src/oclif/auth.js"; +import { checkExistingLogin } from "../../src/oclif/commands/login.js"; + +const CREDENTIALS: StoredCliCredentials = { + api_key: "prim_existing", + base_url: "https://www.primitive.dev/api/v1", + created_at: "2026-05-05T00:00:00.000Z", + key_id: "11111111-1111-4111-8111-111111111111", + key_prefix: "prim_exi...", + org_id: "22222222-2222-4222-8222-222222222222", + org_name: "Acme", +}; + +describe("checkExistingLogin", () => { + let tempDir: string; + let writeSpy: ReturnType; + + beforeEach(() => { + tempDir = mkdtempSync(join(tmpdir(), "primitive-cli-login-test-")); + writeSpy = vi.spyOn(process.stderr, "write").mockImplementation(() => true); + }); + + afterEach(() => { + writeSpy.mockRestore(); + rmSync(tempDir, { force: true, recursive: true }); + }); + + it("reports a valid saved login without removing credentials", async () => { + saveCliCredentials(tempDir, CREDENTIALS); + + const result = await checkExistingLogin({ + configDir: tempDir, + credentials: CREDENTIALS, + checkAccount: async () => ({}), + }); + + expect(result).toEqual({ status: "valid" }); + expect(loadCliCredentials(tempDir)).toEqual(CREDENTIALS); + }); + + it("removes stale saved credentials and allows login to continue", async () => { + saveCliCredentials(tempDir, CREDENTIALS); + + const result = await checkExistingLogin({ + configDir: tempDir, + credentials: CREDENTIALS, + checkAccount: async () => ({ + error: { code: "unauthorized", message: "Invalid API key" }, + }), + }); + + expect(result).toEqual({ status: "removed_stale" }); + expect(loadCliCredentials(tempDir)).toBeNull(); + }); + + it("keeps saved credentials when a different base URL rejects them", async () => { + saveCliCredentials(tempDir, CREDENTIALS); + + const result = await checkExistingLogin({ + baseUrl: "http://localhost:3000/api/v1", + configDir: tempDir, + credentials: CREDENTIALS, + checkAccount: async () => ({ + error: { code: "unauthorized", message: "Invalid API key" }, + }), + }); + + expect(result.status).toBe("blocked"); + expect(loadCliCredentials(tempDir)).toEqual(CREDENTIALS); + }); + + it("keeps saved credentials when verification fails for a non-auth reason", async () => { + saveCliCredentials(tempDir, CREDENTIALS); + + const result = await checkExistingLogin({ + configDir: tempDir, + credentials: CREDENTIALS, + checkAccount: async () => ({ + error: { code: "server_error", message: "Primitive is unavailable" }, + }), + }); + + expect(result.status).toBe("blocked"); + expect(loadCliCredentials(tempDir)).toEqual(CREDENTIALS); + }); + + it("removes stale credentials when the explicit base URL matches the saved one", async () => { + saveCliCredentials(tempDir, CREDENTIALS); + + const result = await checkExistingLogin({ + baseUrl: `${CREDENTIALS.base_url}/`, + configDir: tempDir, + credentials: CREDENTIALS, + checkAccount: async () => ({ + error: { code: "unauthorized", message: "Invalid API key" }, + }), + }); + + expect(result).toEqual({ status: "removed_stale" }); + expect(loadCliCredentials(tempDir)).toBeNull(); + }); +}); diff --git a/sdk-python/src/primitive/api/api/cli/__init__.py b/sdk-python/src/primitive/api/api/cli/__init__.py new file mode 100644 index 0000000..c9921b5 --- /dev/null +++ b/sdk-python/src/primitive/api/api/cli/__init__.py @@ -0,0 +1 @@ +""" Contains endpoint functions for accessing the API """ diff --git a/sdk-python/src/primitive/api/api/cli/cli_logout.py b/sdk-python/src/primitive/api/api/cli/cli_logout.py new file mode 100644 index 0000000..33319ef --- /dev/null +++ b/sdk-python/src/primitive/api/api/cli/cli_logout.py @@ -0,0 +1,223 @@ +from http import HTTPStatus +from typing import Any, cast +from urllib.parse import quote + +import httpx + +from ...client import AuthenticatedClient, Client +from ...types import Response, UNSET +from ... import errors + +from ...models.cli_logout_input import CliLogoutInput +from ...models.cli_logout_response_200 import CliLogoutResponse200 +from ...models.error_response import ErrorResponse +from ...types import UNSET, Unset +from typing import cast + + + +def _get_kwargs( + *, + body: CliLogoutInput | Unset = UNSET, + +) -> dict[str, Any]: + headers: dict[str, Any] = {} + + + + + + + _kwargs: dict[str, Any] = { + "method": "post", + "url": "/cli/logout", + } + + + if not isinstance(body, Unset): + _kwargs["json"] = body.to_dict() + + + headers["Content-Type"] = "application/json" + + _kwargs["headers"] = headers + return _kwargs + + + +def _parse_response(*, client: AuthenticatedClient | Client, response: httpx.Response) -> CliLogoutResponse200 | ErrorResponse | None: + if response.status_code == 200: + response_200 = CliLogoutResponse200.from_dict(response.json()) + + + + return response_200 + + if response.status_code == 400: + response_400 = ErrorResponse.from_dict(response.json()) + + + + return response_400 + + if response.status_code == 401: + response_401 = ErrorResponse.from_dict(response.json()) + + + + return response_401 + + if response.status_code == 403: + response_403 = ErrorResponse.from_dict(response.json()) + + + + return response_403 + + if response.status_code == 404: + response_404 = ErrorResponse.from_dict(response.json()) + + + + return response_404 + + if client.raise_on_unexpected_status: + raise errors.UnexpectedStatus(response.status_code, response.content) + else: + return None + + +def _build_response(*, client: AuthenticatedClient | Client, response: httpx.Response) -> Response[CliLogoutResponse200 | ErrorResponse]: + return Response( + status_code=HTTPStatus(response.status_code), + content=response.content, + headers=response.headers, + parsed=_parse_response(client=client, response=response), + ) + + +def sync_detailed( + *, + client: AuthenticatedClient | Client, + body: CliLogoutInput | Unset = UNSET, + +) -> Response[CliLogoutResponse200 | ErrorResponse]: + """ Revoke the current CLI API key + + Revokes the API key used to authenticate the request. CLI clients use + this endpoint during `primitive logout` before removing local credentials. + + Args: + body (CliLogoutInput | Unset): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[CliLogoutResponse200 | ErrorResponse] + """ + + + kwargs = _get_kwargs( + body=body, + + ) + + response = client.get_httpx_client().request( + **kwargs, + ) + + return _build_response(client=client, response=response) + +def sync( + *, + client: AuthenticatedClient | Client, + body: CliLogoutInput | Unset = UNSET, + +) -> CliLogoutResponse200 | ErrorResponse | None: + """ Revoke the current CLI API key + + Revokes the API key used to authenticate the request. CLI clients use + this endpoint during `primitive logout` before removing local credentials. + + Args: + body (CliLogoutInput | Unset): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + CliLogoutResponse200 | ErrorResponse + """ + + + return sync_detailed( + client=client, +body=body, + + ).parsed + +async def asyncio_detailed( + *, + client: AuthenticatedClient | Client, + body: CliLogoutInput | Unset = UNSET, + +) -> Response[CliLogoutResponse200 | ErrorResponse]: + """ Revoke the current CLI API key + + Revokes the API key used to authenticate the request. CLI clients use + this endpoint during `primitive logout` before removing local credentials. + + Args: + body (CliLogoutInput | Unset): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[CliLogoutResponse200 | ErrorResponse] + """ + + + kwargs = _get_kwargs( + body=body, + + ) + + response = await client.get_async_httpx_client().request( + **kwargs + ) + + return _build_response(client=client, response=response) + +async def asyncio( + *, + client: AuthenticatedClient | Client, + body: CliLogoutInput | Unset = UNSET, + +) -> CliLogoutResponse200 | ErrorResponse | None: + """ Revoke the current CLI API key + + Revokes the API key used to authenticate the request. CLI clients use + this endpoint during `primitive logout` before removing local credentials. + + Args: + body (CliLogoutInput | Unset): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + CliLogoutResponse200 | ErrorResponse + """ + + + return (await asyncio_detailed( + client=client, +body=body, + + )).parsed diff --git a/sdk-python/src/primitive/api/api/cli/poll_cli_login.py b/sdk-python/src/primitive/api/api/cli/poll_cli_login.py new file mode 100644 index 0000000..7afdd6a --- /dev/null +++ b/sdk-python/src/primitive/api/api/cli/poll_cli_login.py @@ -0,0 +1,217 @@ +from http import HTTPStatus +from typing import Any, cast +from urllib.parse import quote + +import httpx + +from ...client import AuthenticatedClient, Client +from ...types import Response, UNSET +from ... import errors + +from ...models.error_response import ErrorResponse +from ...models.poll_cli_login_input import PollCliLoginInput +from ...models.poll_cli_login_response_200 import PollCliLoginResponse200 +from typing import cast + + + +def _get_kwargs( + *, + body: PollCliLoginInput, + +) -> dict[str, Any]: + headers: dict[str, Any] = {} + + + + + + + _kwargs: dict[str, Any] = { + "method": "post", + "url": "/cli/login/poll", + } + + _kwargs["json"] = body.to_dict() + + + headers["Content-Type"] = "application/json" + + _kwargs["headers"] = headers + return _kwargs + + + +def _parse_response(*, client: AuthenticatedClient | Client, response: httpx.Response) -> ErrorResponse | PollCliLoginResponse200 | None: + if response.status_code == 200: + response_200 = PollCliLoginResponse200.from_dict(response.json()) + + + + return response_200 + + if response.status_code == 400: + response_400 = ErrorResponse.from_dict(response.json()) + + + + return response_400 + + if response.status_code == 403: + response_403 = ErrorResponse.from_dict(response.json()) + + + + return response_403 + + if response.status_code == 429: + response_429 = ErrorResponse.from_dict(response.json()) + + + + return response_429 + + if client.raise_on_unexpected_status: + raise errors.UnexpectedStatus(response.status_code, response.content) + else: + return None + + +def _build_response(*, client: AuthenticatedClient | Client, response: httpx.Response) -> Response[ErrorResponse | PollCliLoginResponse200]: + return Response( + status_code=HTTPStatus(response.status_code), + content=response.content, + headers=response.headers, + parsed=_parse_response(client=client, response=response), + ) + + +def sync_detailed( + *, + client: AuthenticatedClient | Client, + body: PollCliLoginInput, + +) -> Response[ErrorResponse | PollCliLoginResponse200]: + """ Poll CLI browser login + + Polls a CLI login session until the browser approval either succeeds, + is denied, expires, or is polled too quickly. The API key is generated + only after approval and is returned exactly once. + + Args: + body (PollCliLoginInput): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[ErrorResponse | PollCliLoginResponse200] + """ + + + kwargs = _get_kwargs( + body=body, + + ) + + response = client.get_httpx_client().request( + **kwargs, + ) + + return _build_response(client=client, response=response) + +def sync( + *, + client: AuthenticatedClient | Client, + body: PollCliLoginInput, + +) -> ErrorResponse | PollCliLoginResponse200 | None: + """ Poll CLI browser login + + Polls a CLI login session until the browser approval either succeeds, + is denied, expires, or is polled too quickly. The API key is generated + only after approval and is returned exactly once. + + Args: + body (PollCliLoginInput): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + ErrorResponse | PollCliLoginResponse200 + """ + + + return sync_detailed( + client=client, +body=body, + + ).parsed + +async def asyncio_detailed( + *, + client: AuthenticatedClient | Client, + body: PollCliLoginInput, + +) -> Response[ErrorResponse | PollCliLoginResponse200]: + """ Poll CLI browser login + + Polls a CLI login session until the browser approval either succeeds, + is denied, expires, or is polled too quickly. The API key is generated + only after approval and is returned exactly once. + + Args: + body (PollCliLoginInput): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[ErrorResponse | PollCliLoginResponse200] + """ + + + kwargs = _get_kwargs( + body=body, + + ) + + response = await client.get_async_httpx_client().request( + **kwargs + ) + + return _build_response(client=client, response=response) + +async def asyncio( + *, + client: AuthenticatedClient | Client, + body: PollCliLoginInput, + +) -> ErrorResponse | PollCliLoginResponse200 | None: + """ Poll CLI browser login + + Polls a CLI login session until the browser approval either succeeds, + is denied, expires, or is polled too quickly. The API key is generated + only after approval and is returned exactly once. + + Args: + body (PollCliLoginInput): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + ErrorResponse | PollCliLoginResponse200 + """ + + + return (await asyncio_detailed( + client=client, +body=body, + + )).parsed diff --git a/sdk-python/src/primitive/api/api/cli/start_cli_login.py b/sdk-python/src/primitive/api/api/cli/start_cli_login.py new file mode 100644 index 0000000..4085ade --- /dev/null +++ b/sdk-python/src/primitive/api/api/cli/start_cli_login.py @@ -0,0 +1,213 @@ +from http import HTTPStatus +from typing import Any, cast +from urllib.parse import quote + +import httpx + +from ...client import AuthenticatedClient, Client +from ...types import Response, UNSET +from ... import errors + +from ...models.error_response import ErrorResponse +from ...models.start_cli_login_input import StartCliLoginInput +from ...models.start_cli_login_response_201 import StartCliLoginResponse201 +from ...types import UNSET, Unset +from typing import cast + + + +def _get_kwargs( + *, + body: StartCliLoginInput | Unset = UNSET, + +) -> dict[str, Any]: + headers: dict[str, Any] = {} + + + + + + + _kwargs: dict[str, Any] = { + "method": "post", + "url": "/cli/login/start", + } + + + if not isinstance(body, Unset): + _kwargs["json"] = body.to_dict() + + + headers["Content-Type"] = "application/json" + + _kwargs["headers"] = headers + return _kwargs + + + +def _parse_response(*, client: AuthenticatedClient | Client, response: httpx.Response) -> ErrorResponse | StartCliLoginResponse201 | None: + if response.status_code == 201: + response_201 = StartCliLoginResponse201.from_dict(response.json()) + + + + return response_201 + + if response.status_code == 400: + response_400 = ErrorResponse.from_dict(response.json()) + + + + return response_400 + + if response.status_code == 429: + response_429 = ErrorResponse.from_dict(response.json()) + + + + return response_429 + + if client.raise_on_unexpected_status: + raise errors.UnexpectedStatus(response.status_code, response.content) + else: + return None + + +def _build_response(*, client: AuthenticatedClient | Client, response: httpx.Response) -> Response[ErrorResponse | StartCliLoginResponse201]: + return Response( + status_code=HTTPStatus(response.status_code), + content=response.content, + headers=response.headers, + parsed=_parse_response(client=client, response=response), + ) + + +def sync_detailed( + *, + client: AuthenticatedClient | Client, + body: StartCliLoginInput | Unset = UNSET, + +) -> Response[ErrorResponse | StartCliLoginResponse201]: + """ Start CLI browser login + + Starts a browser-assisted CLI login session. The response includes a + device code for polling and a user code that the user approves in the + browser. This endpoint does not require an API key. + + Args: + body (StartCliLoginInput | Unset): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[ErrorResponse | StartCliLoginResponse201] + """ + + + kwargs = _get_kwargs( + body=body, + + ) + + response = client.get_httpx_client().request( + **kwargs, + ) + + return _build_response(client=client, response=response) + +def sync( + *, + client: AuthenticatedClient | Client, + body: StartCliLoginInput | Unset = UNSET, + +) -> ErrorResponse | StartCliLoginResponse201 | None: + """ Start CLI browser login + + Starts a browser-assisted CLI login session. The response includes a + device code for polling and a user code that the user approves in the + browser. This endpoint does not require an API key. + + Args: + body (StartCliLoginInput | Unset): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + ErrorResponse | StartCliLoginResponse201 + """ + + + return sync_detailed( + client=client, +body=body, + + ).parsed + +async def asyncio_detailed( + *, + client: AuthenticatedClient | Client, + body: StartCliLoginInput | Unset = UNSET, + +) -> Response[ErrorResponse | StartCliLoginResponse201]: + """ Start CLI browser login + + Starts a browser-assisted CLI login session. The response includes a + device code for polling and a user code that the user approves in the + browser. This endpoint does not require an API key. + + Args: + body (StartCliLoginInput | Unset): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[ErrorResponse | StartCliLoginResponse201] + """ + + + kwargs = _get_kwargs( + body=body, + + ) + + response = await client.get_async_httpx_client().request( + **kwargs + ) + + return _build_response(client=client, response=response) + +async def asyncio( + *, + client: AuthenticatedClient | Client, + body: StartCliLoginInput | Unset = UNSET, + +) -> ErrorResponse | StartCliLoginResponse201 | None: + """ Start CLI browser login + + Starts a browser-assisted CLI login session. The response includes a + device code for polling and a user code that the user approves in the + browser. This endpoint does not require an API key. + + Args: + body (StartCliLoginInput | Unset): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + ErrorResponse | StartCliLoginResponse201 + """ + + + return (await asyncio_detailed( + client=client, +body=body, + + )).parsed diff --git a/sdk-python/src/primitive/api/models/__init__.py b/sdk-python/src/primitive/api/models/__init__.py index 41bcc63..2df72e0 100644 --- a/sdk-python/src/primitive/api/models/__init__.py +++ b/sdk-python/src/primitive/api/models/__init__.py @@ -4,6 +4,11 @@ from .account_updated import AccountUpdated from .add_domain_input import AddDomainInput from .add_domain_response_201 import AddDomainResponse201 +from .cli_login_poll_result import CliLoginPollResult +from .cli_login_start_result import CliLoginStartResult +from .cli_logout_input import CliLogoutInput +from .cli_logout_response_200 import CliLogoutResponse200 +from .cli_logout_result import CliLogoutResult from .create_endpoint_input import CreateEndpointInput from .create_endpoint_input_rules import CreateEndpointInputRules from .create_endpoint_response_201 import CreateEndpointResponse201 @@ -62,6 +67,8 @@ from .list_filters_response_200 import ListFiltersResponse200 from .list_sent_emails_response_200 import ListSentEmailsResponse200 from .pagination_meta import PaginationMeta +from .poll_cli_login_input import PollCliLoginInput +from .poll_cli_login_response_200 import PollCliLoginResponse200 from .replay_delivery_response_200 import ReplayDeliveryResponse200 from .replay_email_webhooks_response_200 import ReplayEmailWebhooksResponse200 from .replay_result import ReplayResult @@ -83,6 +90,9 @@ from .sent_email_detail import SentEmailDetail from .sent_email_status import SentEmailStatus from .sent_email_summary import SentEmailSummary +from .start_cli_login_input import StartCliLoginInput +from .start_cli_login_input_metadata import StartCliLoginInputMetadata +from .start_cli_login_response_201 import StartCliLoginResponse201 from .storage_stats import StorageStats from .success_envelope import SuccessEnvelope from .test_endpoint_response_200 import TestEndpointResponse200 @@ -106,6 +116,11 @@ "AccountUpdated", "AddDomainInput", "AddDomainResponse201", + "CliLoginPollResult", + "CliLoginStartResult", + "CliLogoutInput", + "CliLogoutResponse200", + "CliLogoutResult", "CreateEndpointInput", "CreateEndpointInputRules", "CreateEndpointResponse201", @@ -164,6 +179,8 @@ "ListFiltersResponse200", "ListSentEmailsResponse200", "PaginationMeta", + "PollCliLoginInput", + "PollCliLoginResponse200", "ReplayDeliveryResponse200", "ReplayEmailWebhooksResponse200", "ReplayResult", @@ -185,6 +202,9 @@ "SentEmailDetail", "SentEmailStatus", "SentEmailSummary", + "StartCliLoginInput", + "StartCliLoginInputMetadata", + "StartCliLoginResponse201", "StorageStats", "SuccessEnvelope", "TestEndpointResponse200", diff --git a/sdk-python/src/primitive/api/models/cli_login_poll_result.py b/sdk-python/src/primitive/api/models/cli_login_poll_result.py new file mode 100644 index 0000000..cd5e770 --- /dev/null +++ b/sdk-python/src/primitive/api/models/cli_login_poll_result.py @@ -0,0 +1,123 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, TypeVar, BinaryIO, TextIO, TYPE_CHECKING, Generator + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +from ..types import UNSET, Unset + +from typing import cast +from uuid import UUID + + + + + + +T = TypeVar("T", bound="CliLoginPollResult") + + + +@_attrs_define +class CliLoginPollResult: + """ + Attributes: + api_key (str): Newly-created API key for CLI authentication + key_id (UUID): + key_prefix (str): + org_id (UUID): + org_name (None | str): + """ + + api_key: str + key_id: UUID + key_prefix: str + org_id: UUID + org_name: None | str + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + + + + + def to_dict(self) -> dict[str, Any]: + api_key = self.api_key + + key_id = str(self.key_id) + + key_prefix = self.key_prefix + + org_id = str(self.org_id) + + org_name: None | str + org_name = self.org_name + + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update({ + "api_key": api_key, + "key_id": key_id, + "key_prefix": key_prefix, + "org_id": org_id, + "org_name": org_name, + }) + + return field_dict + + + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + d = dict(src_dict) + api_key = d.pop("api_key") + + key_id = UUID(d.pop("key_id")) + + + + + key_prefix = d.pop("key_prefix") + + org_id = UUID(d.pop("org_id")) + + + + + def _parse_org_name(data: object) -> None | str: + if data is None: + return data + return cast(None | str, data) + + org_name = _parse_org_name(d.pop("org_name")) + + + cli_login_poll_result = cls( + api_key=api_key, + key_id=key_id, + key_prefix=key_prefix, + org_id=org_id, + org_name=org_name, + ) + + + cli_login_poll_result.additional_properties = d + return cli_login_poll_result + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/sdk-python/src/primitive/api/models/cli_login_start_result.py b/sdk-python/src/primitive/api/models/cli_login_start_result.py new file mode 100644 index 0000000..1a11f2d --- /dev/null +++ b/sdk-python/src/primitive/api/models/cli_login_start_result.py @@ -0,0 +1,116 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, TypeVar, BinaryIO, TextIO, TYPE_CHECKING, Generator + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +from ..types import UNSET, Unset + + + + + + + +T = TypeVar("T", bound="CliLoginStartResult") + + + +@_attrs_define +class CliLoginStartResult: + """ + Attributes: + device_code (str): Opaque code used by the CLI to poll for approval + user_code (str): Short code the user confirms in the browser + verification_uri (str): Browser URL where the user approves the login + verification_uri_complete (str): Browser URL with the user code prefilled + expires_in (int): Seconds until the login session expires + interval (int): Minimum seconds between poll requests + """ + + device_code: str + user_code: str + verification_uri: str + verification_uri_complete: str + expires_in: int + interval: int + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + + + + + def to_dict(self) -> dict[str, Any]: + device_code = self.device_code + + user_code = self.user_code + + verification_uri = self.verification_uri + + verification_uri_complete = self.verification_uri_complete + + expires_in = self.expires_in + + interval = self.interval + + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update({ + "device_code": device_code, + "user_code": user_code, + "verification_uri": verification_uri, + "verification_uri_complete": verification_uri_complete, + "expires_in": expires_in, + "interval": interval, + }) + + return field_dict + + + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + d = dict(src_dict) + device_code = d.pop("device_code") + + user_code = d.pop("user_code") + + verification_uri = d.pop("verification_uri") + + verification_uri_complete = d.pop("verification_uri_complete") + + expires_in = d.pop("expires_in") + + interval = d.pop("interval") + + cli_login_start_result = cls( + device_code=device_code, + user_code=user_code, + verification_uri=verification_uri, + verification_uri_complete=verification_uri_complete, + expires_in=expires_in, + interval=interval, + ) + + + cli_login_start_result.additional_properties = d + return cli_login_start_result + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/sdk-python/src/primitive/api/models/cli_logout_input.py b/sdk-python/src/primitive/api/models/cli_logout_input.py new file mode 100644 index 0000000..8096067 --- /dev/null +++ b/sdk-python/src/primitive/api/models/cli_logout_input.py @@ -0,0 +1,70 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, TypeVar, BinaryIO, TextIO, TYPE_CHECKING, Generator + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +from ..types import UNSET, Unset + +from uuid import UUID + + + + + + +T = TypeVar("T", bound="CliLogoutInput") + + + +@_attrs_define +class CliLogoutInput: + """ + Attributes: + key_id (UUID | Unset): Optional key id guard; when provided it must match the authenticated API key + """ + + key_id: UUID | Unset = UNSET + + + + + + def to_dict(self) -> dict[str, Any]: + key_id: str | Unset = UNSET + if not isinstance(self.key_id, Unset): + key_id = str(self.key_id) + + + field_dict: dict[str, Any] = {} + + field_dict.update({ + }) + if key_id is not UNSET: + field_dict["key_id"] = key_id + + return field_dict + + + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + d = dict(src_dict) + _key_id = d.pop("key_id", UNSET) + key_id: UUID | Unset + if isinstance(_key_id, Unset): + key_id = UNSET + else: + key_id = UUID(_key_id) + + + + + cli_logout_input = cls( + key_id=key_id, + ) + + return cli_logout_input + diff --git a/sdk-python/src/primitive/api/models/cli_logout_response_200.py b/sdk-python/src/primitive/api/models/cli_logout_response_200.py new file mode 100644 index 0000000..82a596b --- /dev/null +++ b/sdk-python/src/primitive/api/models/cli_logout_response_200.py @@ -0,0 +1,100 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, TypeVar, BinaryIO, TextIO, TYPE_CHECKING, Generator + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +from ..types import UNSET, Unset + +from typing import cast + +if TYPE_CHECKING: + from ..models.cli_logout_result import CliLogoutResult + + + + + +T = TypeVar("T", bound="CliLogoutResponse200") + + + +@_attrs_define +class CliLogoutResponse200: + """ + Attributes: + success (bool): + data (CliLogoutResult | Unset): + """ + + success: bool + data: CliLogoutResult | Unset = UNSET + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + + + + + def to_dict(self) -> dict[str, Any]: + from ..models.cli_logout_result import CliLogoutResult + success = self.success + + data: dict[str, Any] | Unset = UNSET + if not isinstance(self.data, Unset): + data = self.data.to_dict() + + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update({ + "success": success, + }) + if data is not UNSET: + field_dict["data"] = data + + return field_dict + + + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + from ..models.cli_logout_result import CliLogoutResult + d = dict(src_dict) + success = d.pop("success") + + _data = d.pop("data", UNSET) + data: CliLogoutResult | Unset + if isinstance(_data, Unset): + data = UNSET + else: + data = CliLogoutResult.from_dict(_data) + + + + + cli_logout_response_200 = cls( + success=success, + data=data, + ) + + + cli_logout_response_200.additional_properties = d + return cli_logout_response_200 + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/sdk-python/src/primitive/api/models/cli_logout_result.py b/sdk-python/src/primitive/api/models/cli_logout_result.py new file mode 100644 index 0000000..3cdcfbc --- /dev/null +++ b/sdk-python/src/primitive/api/models/cli_logout_result.py @@ -0,0 +1,88 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, TypeVar, BinaryIO, TextIO, TYPE_CHECKING, Generator + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +from ..types import UNSET, Unset + +from uuid import UUID + + + + + + +T = TypeVar("T", bound="CliLogoutResult") + + + +@_attrs_define +class CliLogoutResult: + """ + Attributes: + revoked (bool): + key_id (UUID): + """ + + revoked: bool + key_id: UUID + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + + + + + def to_dict(self) -> dict[str, Any]: + revoked = self.revoked + + key_id = str(self.key_id) + + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update({ + "revoked": revoked, + "key_id": key_id, + }) + + return field_dict + + + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + d = dict(src_dict) + revoked = d.pop("revoked") + + key_id = UUID(d.pop("key_id")) + + + + + cli_logout_result = cls( + revoked=revoked, + key_id=key_id, + ) + + + cli_logout_result.additional_properties = d + return cli_logout_result + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/sdk-python/src/primitive/api/models/error_response_error_code.py b/sdk-python/src/primitive/api/models/error_response_error_code.py index 961733b..06b6037 100644 --- a/sdk-python/src/primitive/api/models/error_response_error_code.py +++ b/sdk-python/src/primitive/api/models/error_response_error_code.py @@ -1,12 +1,16 @@ from enum import Enum class ErrorResponseErrorCode(str, Enum): + ACCESS_DENIED = "access_denied" + AUTHORIZATION_PENDING = "authorization_pending" CANNOT_SEND_FROM_DOMAIN = "cannot_send_from_domain" CONFLICT = "conflict" DISCARD_NOT_ENABLED = "discard_not_enabled" + EXPIRED_TOKEN = "expired_token" FORBIDDEN = "forbidden" INBOUND_NOT_REPLIABLE = "inbound_not_repliable" INTERNAL_ERROR = "internal_error" + INVALID_DEVICE_CODE = "invalid_device_code" MX_CONFLICT = "mx_conflict" NOT_FOUND = "not_found" OUTBOUND_CAPACITY_EXHAUSTED = "outbound_capacity_exhausted" @@ -18,6 +22,7 @@ class ErrorResponseErrorCode(str, Enum): OUTBOUND_UNREACHABLE = "outbound_unreachable" RATE_LIMIT_EXCEEDED = "rate_limit_exceeded" RECIPIENT_NOT_ALLOWED = "recipient_not_allowed" + SLOW_DOWN = "slow_down" UNAUTHORIZED = "unauthorized" VALIDATION_ERROR = "validation_error" diff --git a/sdk-python/src/primitive/api/models/poll_cli_login_input.py b/sdk-python/src/primitive/api/models/poll_cli_login_input.py new file mode 100644 index 0000000..d456955 --- /dev/null +++ b/sdk-python/src/primitive/api/models/poll_cli_login_input.py @@ -0,0 +1,58 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, TypeVar, BinaryIO, TextIO, TYPE_CHECKING, Generator + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +from ..types import UNSET, Unset + + + + + + + +T = TypeVar("T", bound="PollCliLoginInput") + + + +@_attrs_define +class PollCliLoginInput: + """ + Attributes: + device_code (str): + """ + + device_code: str + + + + + + def to_dict(self) -> dict[str, Any]: + device_code = self.device_code + + + field_dict: dict[str, Any] = {} + + field_dict.update({ + "device_code": device_code, + }) + + return field_dict + + + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + d = dict(src_dict) + device_code = d.pop("device_code") + + poll_cli_login_input = cls( + device_code=device_code, + ) + + return poll_cli_login_input + diff --git a/sdk-python/src/primitive/api/models/poll_cli_login_response_200.py b/sdk-python/src/primitive/api/models/poll_cli_login_response_200.py new file mode 100644 index 0000000..355a34e --- /dev/null +++ b/sdk-python/src/primitive/api/models/poll_cli_login_response_200.py @@ -0,0 +1,100 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, TypeVar, BinaryIO, TextIO, TYPE_CHECKING, Generator + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +from ..types import UNSET, Unset + +from typing import cast + +if TYPE_CHECKING: + from ..models.cli_login_poll_result import CliLoginPollResult + + + + + +T = TypeVar("T", bound="PollCliLoginResponse200") + + + +@_attrs_define +class PollCliLoginResponse200: + """ + Attributes: + success (bool): + data (CliLoginPollResult | Unset): + """ + + success: bool + data: CliLoginPollResult | Unset = UNSET + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + + + + + def to_dict(self) -> dict[str, Any]: + from ..models.cli_login_poll_result import CliLoginPollResult + success = self.success + + data: dict[str, Any] | Unset = UNSET + if not isinstance(self.data, Unset): + data = self.data.to_dict() + + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update({ + "success": success, + }) + if data is not UNSET: + field_dict["data"] = data + + return field_dict + + + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + from ..models.cli_login_poll_result import CliLoginPollResult + d = dict(src_dict) + success = d.pop("success") + + _data = d.pop("data", UNSET) + data: CliLoginPollResult | Unset + if isinstance(_data, Unset): + data = UNSET + else: + data = CliLoginPollResult.from_dict(_data) + + + + + poll_cli_login_response_200 = cls( + success=success, + data=data, + ) + + + poll_cli_login_response_200.additional_properties = d + return poll_cli_login_response_200 + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/sdk-python/src/primitive/api/models/start_cli_login_input.py b/sdk-python/src/primitive/api/models/start_cli_login_input.py new file mode 100644 index 0000000..1ed7b0f --- /dev/null +++ b/sdk-python/src/primitive/api/models/start_cli_login_input.py @@ -0,0 +1,83 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, TypeVar, BinaryIO, TextIO, TYPE_CHECKING, Generator + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +from ..types import UNSET, Unset + +from typing import cast + +if TYPE_CHECKING: + from ..models.start_cli_login_input_metadata import StartCliLoginInputMetadata + + + + + +T = TypeVar("T", bound="StartCliLoginInput") + + + +@_attrs_define +class StartCliLoginInput: + """ + Attributes: + device_name (str | Unset): Human-readable device name shown during browser approval + metadata (StartCliLoginInputMetadata | Unset): Optional client metadata stored with the login session + """ + + device_name: str | Unset = UNSET + metadata: StartCliLoginInputMetadata | Unset = UNSET + + + + + + def to_dict(self) -> dict[str, Any]: + from ..models.start_cli_login_input_metadata import StartCliLoginInputMetadata + device_name = self.device_name + + metadata: dict[str, Any] | Unset = UNSET + if not isinstance(self.metadata, Unset): + metadata = self.metadata.to_dict() + + + field_dict: dict[str, Any] = {} + + field_dict.update({ + }) + if device_name is not UNSET: + field_dict["device_name"] = device_name + if metadata is not UNSET: + field_dict["metadata"] = metadata + + return field_dict + + + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + from ..models.start_cli_login_input_metadata import StartCliLoginInputMetadata + d = dict(src_dict) + device_name = d.pop("device_name", UNSET) + + _metadata = d.pop("metadata", UNSET) + metadata: StartCliLoginInputMetadata | Unset + if isinstance(_metadata, Unset): + metadata = UNSET + else: + metadata = StartCliLoginInputMetadata.from_dict(_metadata) + + + + + start_cli_login_input = cls( + device_name=device_name, + metadata=metadata, + ) + + return start_cli_login_input + diff --git a/sdk-python/src/primitive/api/models/start_cli_login_input_metadata.py b/sdk-python/src/primitive/api/models/start_cli_login_input_metadata.py new file mode 100644 index 0000000..4bb02e0 --- /dev/null +++ b/sdk-python/src/primitive/api/models/start_cli_login_input_metadata.py @@ -0,0 +1,66 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, TypeVar, BinaryIO, TextIO, TYPE_CHECKING, Generator + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +from ..types import UNSET, Unset + + + + + + + +T = TypeVar("T", bound="StartCliLoginInputMetadata") + + + +@_attrs_define +class StartCliLoginInputMetadata: + """ Optional client metadata stored with the login session + + """ + + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + + + + + def to_dict(self) -> dict[str, Any]: + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + + return field_dict + + + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + d = dict(src_dict) + start_cli_login_input_metadata = cls( + ) + + + start_cli_login_input_metadata.additional_properties = d + return start_cli_login_input_metadata + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/sdk-python/src/primitive/api/models/start_cli_login_response_201.py b/sdk-python/src/primitive/api/models/start_cli_login_response_201.py new file mode 100644 index 0000000..70c59e6 --- /dev/null +++ b/sdk-python/src/primitive/api/models/start_cli_login_response_201.py @@ -0,0 +1,100 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, TypeVar, BinaryIO, TextIO, TYPE_CHECKING, Generator + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +from ..types import UNSET, Unset + +from typing import cast + +if TYPE_CHECKING: + from ..models.cli_login_start_result import CliLoginStartResult + + + + + +T = TypeVar("T", bound="StartCliLoginResponse201") + + + +@_attrs_define +class StartCliLoginResponse201: + """ + Attributes: + success (bool): + data (CliLoginStartResult | Unset): + """ + + success: bool + data: CliLoginStartResult | Unset = UNSET + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + + + + + def to_dict(self) -> dict[str, Any]: + from ..models.cli_login_start_result import CliLoginStartResult + success = self.success + + data: dict[str, Any] | Unset = UNSET + if not isinstance(self.data, Unset): + data = self.data.to_dict() + + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update({ + "success": success, + }) + if data is not UNSET: + field_dict["data"] = data + + return field_dict + + + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + from ..models.cli_login_start_result import CliLoginStartResult + d = dict(src_dict) + success = d.pop("success") + + _data = d.pop("data", UNSET) + data: CliLoginStartResult | Unset + if isinstance(_data, Unset): + data = UNSET + else: + data = CliLoginStartResult.from_dict(_data) + + + + + start_cli_login_response_201 = cls( + success=success, + data=data, + ) + + + start_cli_login_response_201.additional_properties = d + return start_cli_login_response_201 + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties From 919edde14ed629d4f5139c11794979e763392a53 Mon Sep 17 00:00:00 2001 From: Jet Date: Tue, 5 May 2026 17:47:33 -0700 Subject: [PATCH 2/6] address greptile review feedback --- sdk-node/src/oclif/commands/login.ts | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/sdk-node/src/oclif/commands/login.ts b/sdk-node/src/oclif/commands/login.ts index 44493ca..b34cb8f 100644 --- a/sdk-node/src/oclif/commands/login.ts +++ b/sdk-node/src/oclif/commands/login.ts @@ -190,12 +190,8 @@ class LoginCommand extends Command { process.stderr.write(`Your login code is: ${start.user_code}\n`); if (!flags["no-browser"]) { - try { - openBrowser(start.verification_uri_complete); - process.stderr.write("Opening Primitive in your browser...\n"); - } catch { - process.stderr.write("Could not open a browser automatically.\n"); - } + openBrowser(start.verification_uri_complete); + process.stderr.write("Opening Primitive in your browser...\n"); } process.stderr.write( `If the browser did not open, visit: ${start.verification_uri_complete}\n`, From a38da57f2a0e5fdb0d5c1f7ac340ce9fef474272 Mon Sep 17 00:00:00 2001 From: Jet Date: Tue, 5 May 2026 17:54:17 -0700 Subject: [PATCH 3/6] Regenerate Go CLI login URI fields Commit the Go artifacts produced by the CI generator so generated-file checks stay in sync. --- sdk-go/api/oas_json_gen.go | 12 ++++++------ sdk-go/api/oas_schemas_gen.go | 13 ++++++------- 2 files changed, 12 insertions(+), 13 deletions(-) diff --git a/sdk-go/api/oas_json_gen.go b/sdk-go/api/oas_json_gen.go index 3a856e9..7567b87 100644 --- a/sdk-go/api/oas_json_gen.go +++ b/sdk-go/api/oas_json_gen.go @@ -964,11 +964,11 @@ func (s *CliLoginStartResult) encodeFields(e *jx.Encoder) { } { e.FieldStart("verification_uri") - json.EncodeURI(e, s.VerificationURI) + e.Str(s.VerificationURI) } { e.FieldStart("verification_uri_complete") - json.EncodeURI(e, s.VerificationURIComplete) + e.Str(s.VerificationURIComplete) } { e.FieldStart("expires_in") @@ -1025,8 +1025,8 @@ func (s *CliLoginStartResult) Decode(d *jx.Decoder) error { case "verification_uri": requiredBitSet[0] |= 1 << 2 if err := func() error { - v, err := json.DecodeURI(d) - s.VerificationURI = v + v, err := d.Str() + s.VerificationURI = string(v) if err != nil { return err } @@ -1037,8 +1037,8 @@ func (s *CliLoginStartResult) Decode(d *jx.Decoder) error { case "verification_uri_complete": requiredBitSet[0] |= 1 << 3 if err := func() error { - v, err := json.DecodeURI(d) - s.VerificationURIComplete = v + v, err := d.Str() + s.VerificationURIComplete = string(v) if err != nil { return err } diff --git a/sdk-go/api/oas_schemas_gen.go b/sdk-go/api/oas_schemas_gen.go index b56340b..4538b67 100644 --- a/sdk-go/api/oas_schemas_gen.go +++ b/sdk-go/api/oas_schemas_gen.go @@ -4,7 +4,6 @@ package api import ( "io" - "net/url" "time" "github.com/go-faster/errors" @@ -355,9 +354,9 @@ type CliLoginStartResult struct { // Short code the user confirms in the browser. UserCode string `json:"user_code"` // Browser URL where the user approves the login. - VerificationURI url.URL `json:"verification_uri"` + VerificationURI string `json:"verification_uri"` // Browser URL with the user code prefilled. - VerificationURIComplete url.URL `json:"verification_uri_complete"` + VerificationURIComplete string `json:"verification_uri_complete"` // Seconds until the login session expires. ExpiresIn int `json:"expires_in"` // Minimum seconds between poll requests. @@ -375,12 +374,12 @@ func (s *CliLoginStartResult) GetUserCode() string { } // GetVerificationURI returns the value of VerificationURI. -func (s *CliLoginStartResult) GetVerificationURI() url.URL { +func (s *CliLoginStartResult) GetVerificationURI() string { return s.VerificationURI } // GetVerificationURIComplete returns the value of VerificationURIComplete. -func (s *CliLoginStartResult) GetVerificationURIComplete() url.URL { +func (s *CliLoginStartResult) GetVerificationURIComplete() string { return s.VerificationURIComplete } @@ -405,12 +404,12 @@ func (s *CliLoginStartResult) SetUserCode(val string) { } // SetVerificationURI sets the value of VerificationURI. -func (s *CliLoginStartResult) SetVerificationURI(val url.URL) { +func (s *CliLoginStartResult) SetVerificationURI(val string) { s.VerificationURI = val } // SetVerificationURIComplete sets the value of VerificationURIComplete. -func (s *CliLoginStartResult) SetVerificationURIComplete(val url.URL) { +func (s *CliLoginStartResult) SetVerificationURIComplete(val string) { s.VerificationURIComplete = val } From 163d10f1a4e21b69e857d535f7ffd02f4d95eb70 Mon Sep 17 00:00:00 2001 From: Jet Date: Wed, 6 May 2026 11:18:33 -0700 Subject: [PATCH 4/6] Update CLI login slow-down contract Move slow_down to the RFC-compatible 400 response and document the CLI login metadata size limit across generated SDK artifacts. --- openapi/primitive-api.codegen.json | 47 +++---- openapi/primitive-api.yaml | 33 ++--- sdk-go/api/oas_json_gen.go | 76 ----------- sdk-go/api/oas_response_decoders_gen.go | 128 ++++++------------ sdk-go/api/oas_response_encoders_gen.go | 43 ++---- sdk-go/api/oas_schemas_gen.go | 13 +- sdk-go/api/oas_validators_gen.go | 16 --- sdk-node/src/api/generated/types.gen.ts | 8 +- sdk-node/src/openapi/openapi.generated.ts | 47 +++---- sdk-node/src/openapi/operations.generated.ts | 2 +- .../primitive/api/api/cli/poll_cli_login.py | 7 - .../api/models/start_cli_login_input.py | 3 +- .../models/start_cli_login_input_metadata.py | 2 +- 13 files changed, 120 insertions(+), 305 deletions(-) diff --git a/openapi/primitive-api.codegen.json b/openapi/primitive-api.codegen.json index 15e5765..4d34510 100644 --- a/openapi/primitive-api.codegen.json +++ b/openapi/primitive-api.codegen.json @@ -169,7 +169,15 @@ } }, "400": { - "description": "Invalid request, pending authorization, expired token, or invalid device code", + "description": "Invalid request, pending authorization, slow polling, expired token, or invalid device code", + "headers": { + "Retry-After": { + "schema": { + "type": "integer" + }, + "description": "Seconds to wait before polling again when the error code is `slow_down`" + } + }, "content": { "application/json": { "schema": { @@ -205,6 +213,16 @@ "message": "Invalid CLI login device code" } } + }, + "slow_down": { + "summary": "Polling too quickly", + "value": { + "success": false, + "error": { + "code": "slow_down", + "message": "Polling too quickly; slow down and retry later" + } + } } } } @@ -226,31 +244,6 @@ } } } - }, - "429": { - "description": "Polling too quickly", - "headers": { - "Retry-After": { - "schema": { - "type": "integer" - }, - "description": "Seconds to wait before polling again" - } - }, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - }, - "example": { - "success": false, - "error": { - "code": "slow_down", - "message": "Polling too quickly; slow down and retry later" - } - } - } - } } } } @@ -2561,7 +2554,7 @@ "metadata": { "type": "object", "additionalProperties": true, - "description": "Optional client metadata stored with the login session" + "description": "Optional client metadata stored with the login session; serialized JSON must be 2048 bytes or fewer" } } }, diff --git a/openapi/primitive-api.yaml b/openapi/primitive-api.yaml index 65da482..d461715 100644 --- a/openapi/primitive-api.yaml +++ b/openapi/primitive-api.yaml @@ -220,7 +220,12 @@ paths: data: $ref: '#/components/schemas/CliLoginPollResult' '400': - description: Invalid request, pending authorization, expired token, or invalid device code + description: Invalid request, pending authorization, slow polling, expired token, or invalid device code + headers: + Retry-After: + schema: + type: integer + description: Seconds to wait before polling again when the error code is `slow_down` content: application/json: schema: @@ -247,6 +252,13 @@ paths: error: code: invalid_device_code message: Invalid CLI login device code + slow_down: + summary: Polling too quickly + value: + success: false + error: + code: slow_down + message: Polling too quickly; slow down and retry later '403': description: CLI login was denied in the browser content: @@ -258,23 +270,6 @@ paths: error: code: access_denied message: CLI login was denied in the browser - '429': - description: Polling too quickly - headers: - Retry-After: - schema: - type: integer - description: Seconds to wait before polling again - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - example: - success: false - error: - code: slow_down - message: Polling too quickly; slow down and retry later - /cli/logout: post: operationId: cliLogout @@ -1955,7 +1950,7 @@ components: metadata: type: object additionalProperties: true - description: Optional client metadata stored with the login session + description: Optional client metadata stored with the login session; serialized JSON must be 2048 bytes or fewer CliLoginStartResult: type: object diff --git a/sdk-go/api/oas_json_gen.go b/sdk-go/api/oas_json_gen.go index 7567b87..5c57129 100644 --- a/sdk-go/api/oas_json_gen.go +++ b/sdk-go/api/oas_json_gen.go @@ -10858,82 +10858,6 @@ func (s *PaginationMeta) UnmarshalJSON(data []byte) error { return s.Decode(d) } -// Encode encodes PollCliLoginBadRequest as json. -func (s *PollCliLoginBadRequest) Encode(e *jx.Encoder) { - unwrapped := (*ErrorResponse)(s) - - unwrapped.Encode(e) -} - -// Decode decodes PollCliLoginBadRequest from json. -func (s *PollCliLoginBadRequest) Decode(d *jx.Decoder) error { - if s == nil { - return errors.New("invalid: unable to decode PollCliLoginBadRequest to nil") - } - var unwrapped ErrorResponse - if err := func() error { - if err := unwrapped.Decode(d); err != nil { - return err - } - return nil - }(); err != nil { - return errors.Wrap(err, "alias") - } - *s = PollCliLoginBadRequest(unwrapped) - return nil -} - -// MarshalJSON implements stdjson.Marshaler. -func (s *PollCliLoginBadRequest) MarshalJSON() ([]byte, error) { - e := jx.Encoder{} - s.Encode(&e) - return e.Bytes(), nil -} - -// UnmarshalJSON implements stdjson.Unmarshaler. -func (s *PollCliLoginBadRequest) UnmarshalJSON(data []byte) error { - d := jx.DecodeBytes(data) - return s.Decode(d) -} - -// Encode encodes PollCliLoginForbidden as json. -func (s *PollCliLoginForbidden) Encode(e *jx.Encoder) { - unwrapped := (*ErrorResponse)(s) - - unwrapped.Encode(e) -} - -// Decode decodes PollCliLoginForbidden from json. -func (s *PollCliLoginForbidden) Decode(d *jx.Decoder) error { - if s == nil { - return errors.New("invalid: unable to decode PollCliLoginForbidden to nil") - } - var unwrapped ErrorResponse - if err := func() error { - if err := unwrapped.Decode(d); err != nil { - return err - } - return nil - }(); err != nil { - return errors.Wrap(err, "alias") - } - *s = PollCliLoginForbidden(unwrapped) - return nil -} - -// MarshalJSON implements stdjson.Marshaler. -func (s *PollCliLoginForbidden) MarshalJSON() ([]byte, error) { - e := jx.Encoder{} - s.Encode(&e) - return e.Bytes(), nil -} - -// UnmarshalJSON implements stdjson.Unmarshaler. -func (s *PollCliLoginForbidden) UnmarshalJSON(data []byte) error { - d := jx.DecodeBytes(data) - return s.Decode(d) -} - // Encode implements json.Marshaler. func (s *PollCliLoginInput) Encode(e *jx.Encoder) { e.ObjStart() diff --git a/sdk-go/api/oas_response_decoders_gen.go b/sdk-go/api/oas_response_decoders_gen.go index 4c3b950..5245305 100644 --- a/sdk-go/api/oas_response_decoders_gen.go +++ b/sdk-go/api/oas_response_decoders_gen.go @@ -3909,7 +3909,7 @@ func decodePollCliLoginResponse(resp *http.Response) (res PollCliLoginRes, _ err } d := jx.DecodeBytes(buf) - var response PollCliLoginBadRequest + var response ErrorResponse if err := func() error { if err := response.Decode(d); err != nil { return err @@ -3935,56 +3935,52 @@ func decodePollCliLoginResponse(resp *http.Response) (res PollCliLoginRes, _ err }(); err != nil { return res, errors.Wrap(err, "validate") } - return &response, nil - default: - return res, validate.InvalidContentType(ct) - } - case 403: - // Code 403. - ct, _, err := mime.ParseMediaType(resp.Header.Get("Content-Type")) - if err != nil { - return res, errors.Wrap(err, "parse media type") - } - switch { - case ct == "application/json": - buf, err := io.ReadAll(resp.Body) - if err != nil { - return res, err - } - d := jx.DecodeBytes(buf) - - var response PollCliLoginForbidden - if err := func() error { - if err := response.Decode(d); err != nil { - return err - } - if err := d.Skip(); err != io.EOF { - return errors.New("unexpected trailing data") - } - return nil - }(); err != nil { - err = &ogenerrors.DecodeBodyError{ - ContentType: ct, - Body: buf, - Err: err, + var wrapper ErrorResponseHeaders + wrapper.Response = response + h := uri.NewHeaderDecoder(resp.Header) + // Parse "Retry-After" header. + { + cfg := uri.HeaderParameterDecodingConfig{ + Name: "Retry-After", + Explode: false, } - return res, err - } - // Validate response. - if err := func() error { - if err := response.Validate(); err != nil { - return err + if err := func() error { + if err := h.HasParam(cfg); err == nil { + if err := h.DecodeParam(cfg, func(d uri.Decoder) error { + var wrapperDotRetryAfterVal int + if err := func() error { + val, err := d.DecodeValue() + if err != nil { + return err + } + + c, err := conv.ToInt(val) + if err != nil { + return err + } + + wrapperDotRetryAfterVal = c + return nil + }(); err != nil { + return err + } + wrapper.RetryAfter.SetTo(wrapperDotRetryAfterVal) + return nil + }); err != nil { + return err + } + } + return nil + }(); err != nil { + return res, errors.Wrap(err, "parse Retry-After header") } - return nil - }(); err != nil { - return res, errors.Wrap(err, "validate") } - return &response, nil + return &wrapper, nil default: return res, validate.InvalidContentType(ct) } - case 429: - // Code 429. + case 403: + // Code 403. ct, _, err := mime.ParseMediaType(resp.Header.Get("Content-Type")) if err != nil { return res, errors.Wrap(err, "parse media type") @@ -4023,47 +4019,7 @@ func decodePollCliLoginResponse(resp *http.Response) (res PollCliLoginRes, _ err }(); err != nil { return res, errors.Wrap(err, "validate") } - var wrapper ErrorResponseHeaders - wrapper.Response = response - h := uri.NewHeaderDecoder(resp.Header) - // Parse "Retry-After" header. - { - cfg := uri.HeaderParameterDecodingConfig{ - Name: "Retry-After", - Explode: false, - } - if err := func() error { - if err := h.HasParam(cfg); err == nil { - if err := h.DecodeParam(cfg, func(d uri.Decoder) error { - var wrapperDotRetryAfterVal int - if err := func() error { - val, err := d.DecodeValue() - if err != nil { - return err - } - - c, err := conv.ToInt(val) - if err != nil { - return err - } - - wrapperDotRetryAfterVal = c - return nil - }(); err != nil { - return err - } - wrapper.RetryAfter.SetTo(wrapperDotRetryAfterVal) - return nil - }); err != nil { - return err - } - } - return nil - }(); err != nil { - return res, errors.Wrap(err, "parse Retry-After header") - } - } - return &wrapper, nil + return &response, nil default: return res, validate.InvalidContentType(ct) } diff --git a/sdk-go/api/oas_response_encoders_gen.go b/sdk-go/api/oas_response_encoders_gen.go index ca7d459..38c10b7 100644 --- a/sdk-go/api/oas_response_encoders_gen.go +++ b/sdk-go/api/oas_response_encoders_gen.go @@ -1371,32 +1371,6 @@ func encodePollCliLoginResponse(response PollCliLoginRes, w http.ResponseWriter, return nil - case *PollCliLoginBadRequest: - w.Header().Set("Content-Type", "application/json; charset=utf-8") - w.WriteHeader(400) - span.SetStatus(codes.Error, http.StatusText(400)) - - e := new(jx.Encoder) - response.Encode(e) - if _, err := e.WriteTo(w); err != nil { - return errors.Wrap(err, "write") - } - - return nil - - case *PollCliLoginForbidden: - w.Header().Set("Content-Type", "application/json; charset=utf-8") - w.WriteHeader(403) - span.SetStatus(codes.Error, http.StatusText(403)) - - e := new(jx.Encoder) - response.Encode(e) - if _, err := e.WriteTo(w); err != nil { - return errors.Wrap(err, "write") - } - - return nil - case *ErrorResponseHeaders: w.Header().Set("Content-Type", "application/json; charset=utf-8") w.Header().Set("Access-Control-Expose-Headers", "Retry-After") @@ -1419,8 +1393,8 @@ func encodePollCliLoginResponse(response PollCliLoginRes, w http.ResponseWriter, } } } - w.WriteHeader(429) - span.SetStatus(codes.Error, http.StatusText(429)) + w.WriteHeader(400) + span.SetStatus(codes.Error, http.StatusText(400)) e := new(jx.Encoder) response.Response.Encode(e) @@ -1430,6 +1404,19 @@ func encodePollCliLoginResponse(response PollCliLoginRes, w http.ResponseWriter, return nil + case *ErrorResponse: + w.Header().Set("Content-Type", "application/json; charset=utf-8") + w.WriteHeader(403) + span.SetStatus(codes.Error, http.StatusText(403)) + + e := new(jx.Encoder) + response.Encode(e) + if _, err := e.WriteTo(w); err != nil { + return errors.Wrap(err, "write") + } + + return nil + default: return errors.Errorf("unexpected response type: %T", response) } diff --git a/sdk-go/api/oas_schemas_gen.go b/sdk-go/api/oas_schemas_gen.go index 4538b67..47c72dd 100644 --- a/sdk-go/api/oas_schemas_gen.go +++ b/sdk-go/api/oas_schemas_gen.go @@ -2604,6 +2604,7 @@ func (*ErrorResponse) getSendPermissionsRes() {} func (*ErrorResponse) listDomainsRes() {} func (*ErrorResponse) listEndpointsRes() {} func (*ErrorResponse) listFiltersRes() {} +func (*ErrorResponse) pollCliLoginRes() {} func (*ErrorResponse) startCliLoginRes() {} type ErrorResponseError struct { @@ -5287,14 +5288,6 @@ func (s *PaginationMeta) SetCursor(val NilString) { s.Cursor = val } -type PollCliLoginBadRequest ErrorResponse - -func (*PollCliLoginBadRequest) pollCliLoginRes() {} - -type PollCliLoginForbidden ErrorResponse - -func (*PollCliLoginForbidden) pollCliLoginRes() {} - // Ref: #/components/schemas/PollCliLoginInput type PollCliLoginInput struct { DeviceCode string `json:"device_code"` @@ -7480,7 +7473,7 @@ func (*StartCliLoginCreatedHeaders) startCliLoginRes() {} type StartCliLoginInput struct { // Human-readable device name shown during browser approval. DeviceName OptString `json:"device_name"` - // Optional client metadata stored with the login session. + // Optional client metadata stored with the login session; serialized JSON must be 2048 bytes or fewer. Metadata OptStartCliLoginInputMetadata `json:"metadata"` } @@ -7504,7 +7497,7 @@ func (s *StartCliLoginInput) SetMetadata(val OptStartCliLoginInputMetadata) { s.Metadata = val } -// Optional client metadata stored with the login session. +// Optional client metadata stored with the login session; serialized JSON must be 2048 bytes or fewer. type StartCliLoginInputMetadata map[string]jx.Raw func (s *StartCliLoginInputMetadata) init() StartCliLoginInputMetadata { diff --git a/sdk-go/api/oas_validators_gen.go b/sdk-go/api/oas_validators_gen.go index 4facc38..f336805 100644 --- a/sdk-go/api/oas_validators_gen.go +++ b/sdk-go/api/oas_validators_gen.go @@ -1649,22 +1649,6 @@ func (s *ListSentEmailsUnauthorized) Validate() error { return nil } -func (s *PollCliLoginBadRequest) Validate() error { - alias := (*ErrorResponse)(s) - if err := alias.Validate(); err != nil { - return err - } - return nil -} - -func (s *PollCliLoginForbidden) Validate() error { - alias := (*ErrorResponse)(s) - if err := alias.Validate(); err != nil { - return err - } - return nil -} - func (s *PollCliLoginInput) Validate() error { if s == nil { return validate.ErrNilPointer diff --git a/sdk-node/src/api/generated/types.gen.ts b/sdk-node/src/api/generated/types.gen.ts index ecebe27..00b13e6 100644 --- a/sdk-node/src/api/generated/types.gen.ts +++ b/sdk-node/src/api/generated/types.gen.ts @@ -123,7 +123,7 @@ export type StartCliLoginInput = { */ device_name?: string; /** - * Optional client metadata stored with the login session + * Optional client metadata stored with the login session; serialized JSON must be 2048 bytes or fewer */ metadata?: { [key: string]: unknown; @@ -1289,17 +1289,13 @@ export type PollCliLoginData = { export type PollCliLoginErrors = { /** - * Invalid request, pending authorization, expired token, or invalid device code + * Invalid request, pending authorization, slow polling, expired token, or invalid device code */ 400: ErrorResponse; /** * CLI login was denied in the browser */ 403: ErrorResponse; - /** - * Polling too quickly - */ - 429: ErrorResponse; }; export type PollCliLoginError = PollCliLoginErrors[keyof PollCliLoginErrors]; diff --git a/sdk-node/src/openapi/openapi.generated.ts b/sdk-node/src/openapi/openapi.generated.ts index bcf4277..3cb0f3c 100644 --- a/sdk-node/src/openapi/openapi.generated.ts +++ b/sdk-node/src/openapi/openapi.generated.ts @@ -176,7 +176,15 @@ export const openapiDocument: Record = { } }, "400": { - "description": "Invalid request, pending authorization, expired token, or invalid device code", + "description": "Invalid request, pending authorization, slow polling, expired token, or invalid device code", + "headers": { + "Retry-After": { + "schema": { + "type": "integer" + }, + "description": "Seconds to wait before polling again when the error code is `slow_down`" + } + }, "content": { "application/json": { "schema": { @@ -212,6 +220,16 @@ export const openapiDocument: Record = { "message": "Invalid CLI login device code" } } + }, + "slow_down": { + "summary": "Polling too quickly", + "value": { + "success": false, + "error": { + "code": "slow_down", + "message": "Polling too quickly; slow down and retry later" + } + } } } } @@ -233,31 +251,6 @@ export const openapiDocument: Record = { } } } - }, - "429": { - "description": "Polling too quickly", - "headers": { - "Retry-After": { - "schema": { - "type": "integer" - }, - "description": "Seconds to wait before polling again" - } - }, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - }, - "example": { - "success": false, - "error": { - "code": "slow_down", - "message": "Polling too quickly; slow down and retry later" - } - } - } - } } } } @@ -2570,7 +2563,7 @@ export const openapiDocument: Record = { "metadata": { "type": "object", "additionalProperties": true, - "description": "Optional client metadata stored with the login session" + "description": "Optional client metadata stored with the login session; serialized JSON must be 2048 bytes or fewer" } } }, diff --git a/sdk-node/src/openapi/operations.generated.ts b/sdk-node/src/openapi/operations.generated.ts index ad48289..4470fec 100644 --- a/sdk-node/src/openapi/operations.generated.ts +++ b/sdk-node/src/openapi/operations.generated.ts @@ -441,7 +441,7 @@ export const operationManifest: PrimitiveOperationManifest[] = [ "metadata": { "type": "object", "additionalProperties": true, - "description": "Optional client metadata stored with the login session" + "description": "Optional client metadata stored with the login session; serialized JSON must be 2048 bytes or fewer" } } }, diff --git a/sdk-python/src/primitive/api/api/cli/poll_cli_login.py b/sdk-python/src/primitive/api/api/cli/poll_cli_login.py index 7afdd6a..451d1e9 100644 --- a/sdk-python/src/primitive/api/api/cli/poll_cli_login.py +++ b/sdk-python/src/primitive/api/api/cli/poll_cli_login.py @@ -64,13 +64,6 @@ def _parse_response(*, client: AuthenticatedClient | Client, response: httpx.Res return response_403 - if response.status_code == 429: - response_429 = ErrorResponse.from_dict(response.json()) - - - - return response_429 - if client.raise_on_unexpected_status: raise errors.UnexpectedStatus(response.status_code, response.content) else: diff --git a/sdk-python/src/primitive/api/models/start_cli_login_input.py b/sdk-python/src/primitive/api/models/start_cli_login_input.py index 1ed7b0f..4e04abf 100644 --- a/sdk-python/src/primitive/api/models/start_cli_login_input.py +++ b/sdk-python/src/primitive/api/models/start_cli_login_input.py @@ -26,7 +26,8 @@ class StartCliLoginInput: """ Attributes: device_name (str | Unset): Human-readable device name shown during browser approval - metadata (StartCliLoginInputMetadata | Unset): Optional client metadata stored with the login session + metadata (StartCliLoginInputMetadata | Unset): Optional client metadata stored with the login session; + serialized JSON must be 2048 bytes or fewer """ device_name: str | Unset = UNSET diff --git a/sdk-python/src/primitive/api/models/start_cli_login_input_metadata.py b/sdk-python/src/primitive/api/models/start_cli_login_input_metadata.py index 4bb02e0..3d0318e 100644 --- a/sdk-python/src/primitive/api/models/start_cli_login_input_metadata.py +++ b/sdk-python/src/primitive/api/models/start_cli_login_input_metadata.py @@ -20,7 +20,7 @@ @_attrs_define class StartCliLoginInputMetadata: - """ Optional client metadata stored with the login session + """ Optional client metadata stored with the login session; serialized JSON must be 2048 bytes or fewer """ From 55ee93013b92b70f44691c2279d70d85f76ee43f Mon Sep 17 00:00:00 2001 From: Jet Date: Wed, 6 May 2026 12:17:03 -0700 Subject: [PATCH 5/6] Harden CLI login credential handling --- sdk-node/src/oclif/api-command.ts | 25 ++++++-- sdk-node/src/oclif/auth.ts | 56 ++++++++++++++-- sdk-node/src/oclif/commands/login.ts | 92 ++++++++++++++++++++++----- sdk-node/src/oclif/commands/logout.ts | 6 +- sdk-node/src/oclif/commands/send.ts | 3 +- sdk-node/tests/oclif/auth.test.ts | 33 +++++++++- 6 files changed, 183 insertions(+), 32 deletions(-) diff --git a/sdk-node/src/oclif/api-command.ts b/sdk-node/src/oclif/api-command.ts index f1a32a0..fea4211 100644 --- a/sdk-node/src/oclif/api-command.ts +++ b/sdk-node/src/oclif/api-command.ts @@ -1,5 +1,6 @@ import { readFileSync, writeFileSync } from "node:fs"; import { Command, Errors, Flags } from "@oclif/core"; +import type { ErrorResponse } from "../api/generated/types.gen.js"; import { operations, PrimitiveApiClient } from "../api/index.js"; import type { PrimitiveOperationManifest, @@ -12,6 +13,17 @@ import { } from "./auth.js"; type OperationName = keyof typeof operations; +export type ApiErrorCode = ErrorResponse["error"]["code"]; + +export const API_ERROR_CODES = { + accessDenied: "access_denied", + authorizationPending: "authorization_pending", + expiredToken: "expired_token", + invalidDeviceCode: "invalid_device_code", + notFound: "not_found", + slowDown: "slow_down", + unauthorized: "unauthorized", +} as const satisfies Record; type OperationExecutor = (options: Record) => Promise<{ data?: Blob | File | Record | Record[]; @@ -377,10 +389,10 @@ export function extractErrorCode(payload: unknown): string | undefined { // alone left the agent without context for the env var or the // `--api-key` flag; this closes that gap without having to // special-case every command. -const ERROR_CODE_HINTS: Record = { - unauthorized: +const ERROR_CODE_HINTS = { + [API_ERROR_CODES.unauthorized]: "Hint: run `primitive login`, pass --api-key explicitly, or set PRIMITIVE_API_KEY in your environment. `primitive whoami` is the fastest way to verify a key is live.", -}; +} as const satisfies Partial>; // Write a server / SDK error to stderr in the canonical envelope // shape, plus an actionable hint when the code is one we know how @@ -390,8 +402,9 @@ const ERROR_CODE_HINTS: Record = { export function writeErrorWithHints(payload: unknown): void { process.stderr.write(`${formatErrorPayload(payload)}\n`); const code = extractErrorCode(payload); - if (code && ERROR_CODE_HINTS[code]) { - process.stderr.write(`${ERROR_CODE_HINTS[code]}\n`); + if (code && code in ERROR_CODE_HINTS) { + const hint = ERROR_CODE_HINTS[code as keyof typeof ERROR_CODE_HINTS]; + process.stderr.write(`${hint}\n`); } } @@ -402,7 +415,7 @@ export function removeStaleSavedCredentialOnUnauthorized(params: { payload: unknown; }): boolean { if ( - extractErrorCode(params.payload) !== "unauthorized" || + extractErrorCode(params.payload) !== API_ERROR_CODES.unauthorized || params.auth.source !== "stored" ) { return false; diff --git a/sdk-node/src/oclif/auth.ts b/sdk-node/src/oclif/auth.ts index fad3c10..172092d 100644 --- a/sdk-node/src/oclif/auth.ts +++ b/sdk-node/src/oclif/auth.ts @@ -1,7 +1,9 @@ +import { randomUUID } from "node:crypto"; import { chmodSync, mkdirSync, readFileSync, + renameSync, rmSync, writeFileSync, } from "node:fs"; @@ -9,6 +11,9 @@ import { join } from "node:path"; import { DEFAULT_BASE_URL } from "../api/index.js"; const CREDENTIALS_FILE = "credentials.json"; +const CREDENTIALS_LOCK_DIR = "credentials.lock"; +const MALFORMED_CREDENTIALS_HINT = + "Run `primitive logout` and then `primitive login`."; export type StoredCliCredentials = { api_key: string; @@ -38,7 +43,7 @@ function requireString( const raw = value[key]; if (typeof raw !== "string" || raw.trim().length === 0) { throw new Error( - "Stored Primitive CLI credentials are malformed. Run `primitive logout` and then `primitive login`.", + `Stored Primitive CLI credentials are malformed: ${key} must be a non-empty string. ${MALFORMED_CREDENTIALS_HINT}`, ); } return raw; @@ -47,14 +52,14 @@ function requireString( function parseCredentials(raw: unknown): StoredCliCredentials { if (!isRecord(raw)) { throw new Error( - "Stored Primitive CLI credentials are malformed. Run `primitive logout` and then `primitive login`.", + `Stored Primitive CLI credentials are malformed: expected a JSON object. ${MALFORMED_CREDENTIALS_HINT}`, ); } const orgName = raw.org_name; if (orgName !== null && typeof orgName !== "string") { throw new Error( - "Stored Primitive CLI credentials are malformed. Run `primitive logout` and then `primitive login`.", + `Stored Primitive CLI credentials are malformed: org_name must be a string or null. ${MALFORMED_CREDENTIALS_HINT}`, ); } @@ -116,16 +121,53 @@ export function saveCliCredentials( ): void { mkdirSync(configDir, { mode: 0o700, recursive: true }); const path = credentialsPath(configDir); - writeFileSync(path, `${JSON.stringify(credentials, null, 2)}\n`, { - mode: 0o600, - }); - chmodSync(path, 0o600); + const tempPath = join( + configDir, + `${CREDENTIALS_FILE}.${process.pid}.${randomUUID()}.tmp`, + ); + try { + writeFileSync(tempPath, `${JSON.stringify(credentials, null, 2)}\n`, { + mode: 0o600, + }); + chmodSync(tempPath, 0o600); + renameSync(tempPath, path); + chmodSync(path, 0o600); + } catch (error) { + rmSync(tempPath, { force: true }); + throw error; + } } export function deleteCliCredentials(configDir: string): void { rmSync(credentialsPath(configDir), { force: true }); } +export function acquireCliCredentialsLock(configDir: string): () => void { + mkdirSync(configDir, { mode: 0o700, recursive: true }); + const lockPath = join(configDir, CREDENTIALS_LOCK_DIR); + try { + mkdirSync(lockPath, { mode: 0o700 }); + } catch (error) { + if ( + error && + typeof error === "object" && + (error as { code?: unknown }).code === "EEXIST" + ) { + throw new Error( + "Another Primitive CLI credential operation is already in progress. Wait for it to finish, then retry.", + ); + } + throw error; + } + + let released = false; + return () => { + if (released) return; + released = true; + rmSync(lockPath, { force: true, recursive: true }); + }; +} + export function resolveCliAuth(params: { configDir: string; apiKey?: string; diff --git a/sdk-node/src/oclif/commands/login.ts b/sdk-node/src/oclif/commands/login.ts index b34cb8f..eae214b 100644 --- a/sdk-node/src/oclif/commands/login.ts +++ b/sdk-node/src/oclif/commands/login.ts @@ -12,18 +12,23 @@ import type { } from "../../api/generated/types.gen.js"; import { PrimitiveApiClient } from "../../api/index.js"; import { + API_ERROR_CODES, extractErrorCode, extractErrorPayload, removeStaleSavedCredentialOnUnauthorized, writeErrorWithHints, } from "../api-command.js"; import { + acquireCliCredentialsLock, + credentialsPath, loadCliCredentials, normalizeBaseUrl, type StoredCliCredentials, saveCliCredentials, } from "../auth.js"; +const MAX_CLI_LOGIN_POLL_INTERVAL_SECONDS = 60; + function cliError(message: string): Errors.CLIError { return new Errors.CLIError(message, { exit: 1 }); } @@ -110,12 +115,19 @@ export async function checkExistingLogin(params: { status: "blocked", payload, message: - code === "unauthorized" + code === API_ERROR_CODES.unauthorized ? "Saved Primitive CLI credentials were rejected. Run `primitive logout` to remove them before logging in again." : "A saved Primitive CLI login exists, but the CLI could not verify whether it is still valid. Run `primitive logout` before logging in again.", }; } +type LoginFlags = { + "base-url"?: string; + "device-name"?: string; + "no-browser"?: boolean; + force?: boolean; +}; + class LoginCommand extends Command { static description = "Log in by opening Primitive in your browser and saving an org-scoped CLI API key locally."; @@ -125,6 +137,7 @@ class LoginCommand extends Command { static examples = [ "<%= config.bin %> login", "<%= config.bin %> login --device-name work-laptop", + "<%= config.bin %> login --force", ]; static flags = { @@ -138,13 +151,50 @@ class LoginCommand extends Command { "no-browser": Flags.boolean({ description: "Do not attempt to open the browser automatically", }), + force: Flags.boolean({ + char: "f", + description: + "Replace saved credentials without first verifying the existing login", + }), }; async run(): Promise { const { flags } = await this.parse(LoginCommand); + + let releaseCredentialsLock: () => void; + try { + releaseCredentialsLock = acquireCliCredentialsLock(this.config.configDir); + } catch (error) { + const detail = error instanceof Error ? error.message : String(error); + throw cliError(detail); + } + + try { + await this.runWithCredentialLock(flags); + } finally { + releaseCredentialsLock(); + } + } + + private async runWithCredentialLock(flags: LoginFlags): Promise { const baseUrl = normalizeBaseUrl(flags["base-url"]); - const existing = loadCliCredentials(this.config.configDir); - if (existing) { + let existing: StoredCliCredentials | null; + try { + existing = loadCliCredentials(this.config.configDir); + } catch (error) { + if (!flags.force) throw error; + const detail = error instanceof Error ? error.message : String(error); + process.stderr.write( + `Replacing unreadable Primitive CLI credentials because --force was set: ${detail}\n`, + ); + existing = null; + } + + if (existing && flags.force) { + process.stderr.write( + "Replacing saved Primitive CLI credentials after browser approval because --force was set.\n", + ); + } else if (existing) { const existingStatus = await checkExistingLogin({ baseUrl: flags["base-url"], configDir: this.config.configDir, @@ -168,11 +218,6 @@ class LoginCommand extends Command { const started = await startCliLogin({ body: { device_name: deviceName, - metadata: { - arch: process.arch, - platform: process.platform, - version: this.config.version, - }, }, client: apiClient.client, responseStyle: "fields", @@ -199,10 +244,15 @@ class LoginCommand extends Command { process.stderr.write("Waiting for browser approval...\n"); const deadline = Date.now() + start.expires_in * 1000; - let interval = start.interval; + let interval = Math.min( + Math.max(1, start.interval), + MAX_CLI_LOGIN_POLL_INTERVAL_SECONDS, + ); + let nextPollDelay = 1; while (Date.now() < deadline) { - await sleep(interval * 1000); + await sleep(nextPollDelay * 1000); + nextPollDelay = interval; const polled = await pollCliLogin({ body: { device_code: start.device_code }, @@ -228,25 +278,35 @@ class LoginCommand extends Command { const org = login.org_name ? ` (${login.org_name})` : ""; process.stderr.write(`Logged in to org ${login.org_id}${org}.\n`); + process.stderr.write( + `Saved credentials to ${credentialsPath(this.config.configDir)}.\n`, + ); return; } const payload = extractErrorPayload(polled.error); const code = extractErrorCode(payload); - if (code === "authorization_pending") continue; - if (code === "slow_down") { - interval = retryAfterSeconds(polled) ?? interval + 5; + if (code === API_ERROR_CODES.authorizationPending) { + nextPollDelay = interval; + continue; + } + if (code === API_ERROR_CODES.slowDown) { + interval = Math.min( + retryAfterSeconds(polled) ?? interval + 5, + MAX_CLI_LOGIN_POLL_INTERVAL_SECONDS, + ); + nextPollDelay = interval; continue; } - if (code === "access_denied") { + if (code === API_ERROR_CODES.accessDenied) { throw cliError("Primitive CLI login was denied in the browser."); } - if (code === "expired_token") { + if (code === API_ERROR_CODES.expiredToken) { throw cliError( "Primitive CLI login expired. Run `primitive login` again.", ); } - if (code === "invalid_device_code") { + if (code === API_ERROR_CODES.invalidDeviceCode) { throw cliError( "Primitive CLI login device code is invalid. Run `primitive login` again.", ); diff --git a/sdk-node/src/oclif/commands/logout.ts b/sdk-node/src/oclif/commands/logout.ts index 9fcc234..3980f53 100644 --- a/sdk-node/src/oclif/commands/logout.ts +++ b/sdk-node/src/oclif/commands/logout.ts @@ -3,6 +3,7 @@ import { cliLogout } from "../../api/generated/sdk.gen.js"; import type { CliLogoutResult } from "../../api/generated/types.gen.js"; import { PrimitiveApiClient } from "../../api/index.js"; import { + API_ERROR_CODES, extractErrorCode, extractErrorPayload, writeErrorWithHints, @@ -74,7 +75,10 @@ class LogoutCommand extends Command { if (result.error) { const payload = extractErrorPayload(result.error); const code = extractErrorCode(payload); - if (code === "unauthorized" || code === "not_found") { + if ( + code === API_ERROR_CODES.unauthorized || + code === API_ERROR_CODES.notFound + ) { deleteCliCredentials(this.config.configDir); writeErrorWithHints(payload); process.stderr.write( diff --git a/sdk-node/src/oclif/commands/send.ts b/sdk-node/src/oclif/commands/send.ts index c7900bd..b79cde3 100644 --- a/sdk-node/src/oclif/commands/send.ts +++ b/sdk-node/src/oclif/commands/send.ts @@ -8,6 +8,7 @@ import type { } from "../../api/generated/types.gen.js"; import { PrimitiveApiClient } from "../../api/index.js"; import { + API_ERROR_CODES, extractErrorCode, extractErrorPayload, formatErrorPayload, @@ -91,7 +92,7 @@ async function pickDefaultFromAddress( // Surface the auth hint via writeErrorWithHints and bail with // a focused message instead of the verbose "underlying error" // wrapping. - if (extractErrorCode(errorPayload) === "unauthorized") { + if (extractErrorCode(errorPayload) === API_ERROR_CODES.unauthorized) { writeErrorWithHints(errorPayload); removeStaleSavedCredentialOnUnauthorized({ ...authFailureContext, diff --git a/sdk-node/tests/oclif/auth.test.ts b/sdk-node/tests/oclif/auth.test.ts index 8c83677..bc5fb34 100644 --- a/sdk-node/tests/oclif/auth.test.ts +++ b/sdk-node/tests/oclif/auth.test.ts @@ -1,8 +1,15 @@ -import { mkdtempSync, rmSync, statSync, writeFileSync } from "node:fs"; +import { + mkdtempSync, + readdirSync, + rmSync, + statSync, + writeFileSync, +} from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { afterEach, beforeEach, describe, expect, it } from "vitest"; import { + acquireCliCredentialsLock, credentialsPath, deleteCliCredentials, loadCliCredentials, @@ -44,6 +51,9 @@ describe("CLI auth credentials", () => { if (process.platform !== "win32") { expect(statSync(credentialsPath(tempDir)).mode & 0o777).toBe(0o600); } + expect( + readdirSync(tempDir).filter((name) => name.endsWith(".tmp")), + ).toEqual([]); }); it("deletes saved credentials", () => { @@ -92,4 +102,25 @@ describe("CLI auth credentials", () => { /credentials are not valid JSON/, ); }); + + it("throws a field-specific error for malformed credential fields", () => { + writeFileSync( + credentialsPath(tempDir), + `${JSON.stringify({ ...CREDENTIALS, api_key: "" })}\n`, + ); + + expect(() => loadCliCredentials(tempDir)).toThrow(/api_key/); + }); + + it("serializes credential updates with a lock directory", () => { + const release = acquireCliCredentialsLock(tempDir); + + expect(() => acquireCliCredentialsLock(tempDir)).toThrow( + /already in progress/, + ); + + release(); + const releaseAgain = acquireCliCredentialsLock(tempDir); + releaseAgain(); + }); }); From d4272359b7a595ed322ab0b0779c7caad9fd37c7 Mon Sep 17 00:00:00 2001 From: Jet Date: Wed, 6 May 2026 14:00:58 -0700 Subject: [PATCH 6/6] Harden CLI credential locking --- sdk-node/src/oclif/auth.ts | 56 ++++++++++++++++++++++----- sdk-node/src/oclif/commands/logout.ts | 21 ++++++++++ sdk-node/tests/oclif/auth.test.ts | 20 ++++++++++ 3 files changed, 87 insertions(+), 10 deletions(-) diff --git a/sdk-node/src/oclif/auth.ts b/sdk-node/src/oclif/auth.ts index 172092d..52d50d2 100644 --- a/sdk-node/src/oclif/auth.ts +++ b/sdk-node/src/oclif/auth.ts @@ -5,6 +5,7 @@ import { readFileSync, renameSync, rmSync, + statSync, writeFileSync, } from "node:fs"; import { join } from "node:path"; @@ -12,6 +13,7 @@ import { DEFAULT_BASE_URL } from "../api/index.js"; const CREDENTIALS_FILE = "credentials.json"; const CREDENTIALS_LOCK_DIR = "credentials.lock"; +const CREDENTIALS_LOCK_STALE_MS = 30 * 60 * 1000; const MALFORMED_CREDENTIALS_HINT = "Run `primitive logout` and then `primitive login`."; @@ -142,22 +144,56 @@ export function deleteCliCredentials(configDir: string): void { rmSync(credentialsPath(configDir), { force: true }); } -export function acquireCliCredentialsLock(configDir: string): () => void { - mkdirSync(configDir, { mode: 0o700, recursive: true }); - const lockPath = join(configDir, CREDENTIALS_LOCK_DIR); +function errorCode(error: unknown): unknown { + return error && typeof error === "object" + ? (error as { code?: unknown }).code + : undefined; +} + +function removeStaleCliCredentialsLock( + lockPath: string, + staleMs: number, + now: () => number, +): boolean { try { - mkdirSync(lockPath, { mode: 0o700 }); + const stats = statSync(lockPath); + if (now() - stats.mtimeMs < staleMs) return false; } catch (error) { - if ( - error && - typeof error === "object" && - (error as { code?: unknown }).code === "EEXIST" - ) { + if (errorCode(error) === "ENOENT") return true; + throw error; + } + + rmSync(lockPath, { force: true, recursive: true }); + return true; +} + +export function acquireCliCredentialsLock( + configDir: string, + options: { now?: () => number; staleMs?: number } = {}, +): () => void { + mkdirSync(configDir, { mode: 0o700, recursive: true }); + const lockPath = join(configDir, CREDENTIALS_LOCK_DIR); + const now = options.now ?? Date.now; + const staleMs = options.staleMs ?? CREDENTIALS_LOCK_STALE_MS; + let acquired = false; + + for (let attempt = 0; attempt < 2; attempt += 1) { + try { + mkdirSync(lockPath, { mode: 0o700 }); + acquired = true; + break; + } catch (error) { + if (errorCode(error) !== "EEXIST") throw error; + if (removeStaleCliCredentialsLock(lockPath, staleMs, now)) continue; throw new Error( "Another Primitive CLI credential operation is already in progress. Wait for it to finish, then retry.", ); } - throw error; + } + if (!acquired) { + throw new Error( + "Another Primitive CLI credential operation is already in progress. Wait for it to finish, then retry.", + ); } let released = false; diff --git a/sdk-node/src/oclif/commands/logout.ts b/sdk-node/src/oclif/commands/logout.ts index 3980f53..360700f 100644 --- a/sdk-node/src/oclif/commands/logout.ts +++ b/sdk-node/src/oclif/commands/logout.ts @@ -9,6 +9,7 @@ import { writeErrorWithHints, } from "../api-command.js"; import { + acquireCliCredentialsLock, deleteCliCredentials, loadCliCredentials, normalizeBaseUrl, @@ -23,6 +24,10 @@ function unwrapData(value: unknown): T | null { return envelope?.data ?? null; } +type LogoutFlags = { + "base-url"?: string; +}; + class LogoutCommand extends Command { static description = "Log out by revoking the saved Primitive CLI API key and deleting local credentials."; @@ -40,6 +45,22 @@ class LogoutCommand extends Command { async run(): Promise { const { flags } = await this.parse(LogoutCommand); + let releaseCredentialsLock: () => void; + try { + releaseCredentialsLock = acquireCliCredentialsLock(this.config.configDir); + } catch (error) { + const detail = error instanceof Error ? error.message : String(error); + throw cliError(detail); + } + + try { + await this.runWithCredentialLock(flags); + } finally { + releaseCredentialsLock(); + } + } + + private async runWithCredentialLock(flags: LogoutFlags): Promise { let credentials: ReturnType; try { credentials = loadCliCredentials(this.config.configDir); diff --git a/sdk-node/tests/oclif/auth.test.ts b/sdk-node/tests/oclif/auth.test.ts index bc5fb34..e434ce6 100644 --- a/sdk-node/tests/oclif/auth.test.ts +++ b/sdk-node/tests/oclif/auth.test.ts @@ -1,8 +1,10 @@ import { + mkdirSync, mkdtempSync, readdirSync, rmSync, statSync, + utimesSync, writeFileSync, } from "node:fs"; import { tmpdir } from "node:os"; @@ -123,4 +125,22 @@ describe("CLI auth credentials", () => { const releaseAgain = acquireCliCredentialsLock(tempDir); releaseAgain(); }); + + it("recovers stale credential lock directories", () => { + const lockPath = join(tempDir, "credentials.lock"); + mkdirSync(lockPath, { mode: 0o700 }); + const now = new Date("2026-05-05T00:00:00.000Z").getTime(); + const staleTime = new Date(now - 2_000); + utimesSync(lockPath, staleTime, staleTime); + + const release = acquireCliCredentialsLock(tempDir, { + now: () => now, + staleMs: 1_000, + }); + + expect(() => + acquireCliCredentialsLock(tempDir, { now: () => now, staleMs: 1_000 }), + ).toThrow(/already in progress/); + release(); + }); });