@offscript/sdk 0.1.2 → 0.1.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/README.md +18 -115
  2. package/dist/adapters/nuxt.cjs +3 -3
  3. package/dist/adapters/nuxt.js +2 -2
  4. package/dist/adapters/vanilla.cjs +5 -5
  5. package/dist/adapters/vanilla.d.ts +2 -3
  6. package/dist/adapters/vanilla.js +3 -3
  7. package/dist/adapters/vue.cjs +3 -3
  8. package/dist/adapters/vue.js +2 -2
  9. package/dist/{chunk-IBEPWI7S.js → chunk-5ZBR6FI5.js} +522 -1231
  10. package/dist/chunk-5ZBR6FI5.js.map +1 -0
  11. package/dist/{chunk-WUFW7TEL.js → chunk-KHKIWCLM.js} +3 -5
  12. package/dist/chunk-KHKIWCLM.js.map +1 -0
  13. package/dist/{chunk-KQVK5EJR.cjs → chunk-MZXF2LF2.cjs} +3 -5
  14. package/dist/chunk-MZXF2LF2.cjs.map +1 -0
  15. package/dist/{chunk-I6JPZAQA.cjs → chunk-PCJRHW66.cjs} +525 -1239
  16. package/dist/chunk-PCJRHW66.cjs.map +1 -0
  17. package/dist/{chunk-HECPMGDC.js → chunk-R2PPPR4K.js} +5 -5
  18. package/dist/chunk-R2PPPR4K.js.map +1 -0
  19. package/dist/chunk-RAUSXUGS.cjs +20 -0
  20. package/dist/chunk-RAUSXUGS.cjs.map +1 -0
  21. package/dist/core/LifecycleManager.d.ts +1 -0
  22. package/dist/core/OffscriptSDK.d.ts +41 -70
  23. package/dist/core/RecordingStore.d.ts +26 -0
  24. package/dist/index.cjs +20 -40
  25. package/dist/index.d.ts +2 -2
  26. package/dist/index.js +3 -3
  27. package/dist/playwright/PlaywrightDataContract.d.ts +0 -7
  28. package/dist/playwright.cjs +13 -13
  29. package/dist/playwright.js +1 -1
  30. package/dist/recorders/ActionRecorder.d.ts +1 -1
  31. package/dist/recorders/ContextRecorder.d.ts +2 -8
  32. package/dist/recorders/HTTPRecorder.d.ts +7 -2
  33. package/dist/types/config.d.ts +9 -45
  34. package/dist/types/events.d.ts +2 -23
  35. package/dist/types/recording.d.ts +20 -0
  36. package/dist/ui/RecordingControls.d.ts +6 -4
  37. package/dist/utils/errors.d.ts +1 -18
  38. package/docs/api-contract.md +4 -53
  39. package/docs/architecture.md +5 -34
  40. package/docs/event-contract.md +3 -24
  41. package/docs/playwright-contract.md +3 -37
  42. package/package.json +2 -2
  43. package/dist/chunk-HECPMGDC.js.map +0 -1
  44. package/dist/chunk-I6JPZAQA.cjs.map +0 -1
  45. package/dist/chunk-IBEPWI7S.js.map +0 -1
  46. package/dist/chunk-KQVK5EJR.cjs.map +0 -1
  47. package/dist/chunk-TBPD2LMI.cjs +0 -20
  48. package/dist/chunk-TBPD2LMI.cjs.map +0 -1
  49. package/dist/chunk-WUFW7TEL.js.map +0 -1
  50. package/dist/core/ApiClient.d.ts +0 -24
  51. package/dist/core/SessionManager.d.ts +0 -45
  52. package/dist/transport/EventQueue.d.ts +0 -28
  53. package/dist/transport/EventTransport.d.ts +0 -84
  54. package/dist/transport/PersistentBuffer.d.ts +0 -60
  55. package/dist/transport/RetryManager.d.ts +0 -9
  56. package/dist/types/session.d.ts +0 -77
package/README.md CHANGED
@@ -1,134 +1,37 @@
1
1
  # @offscript/sdk
2
2
 
3
- SDK de instrumentação para aplicações web testadas manualmente por QA. Captura, de forma modular e agnóstica de framework, tudo o que é necessário para **reproduzir** a sessão (rrweb), **investigar** (ações, rede, erros, contexto) e, no futuro, **automatizar** (contrato Playwright), enviando os eventos para a API da Offscript.
4
-
5
- Funciona em JS puro, Vue 2/3, Nuxt, React, Laravel Blade e Inertia, em SPA e MPA. O núcleo não depende de nenhum framework.
6
-
7
- > Estado: `0.1.0`. Veja [Limitações conhecidas](#limitações-conhecidas) e o [relatório de testes](#testes).
8
-
9
- ## Instalação
10
-
11
- ```bash
12
- npm install @offscript/sdk
13
- ```
14
-
15
- ## Uso
3
+ Biblioteca de captura no navegador: DOM com rrweb, ações, navegação SPA, HTTP, console, erros, contexto e iframes. O SDK lê `offscript_token` da query string, mantém o token e a gravação entre documentos da mesma origem e preserva a URL. O overlay só aparece quando há um token de execução.
16
4
 
17
5
  ```ts
18
6
  import { Offscript } from "@offscript/sdk";
19
7
 
20
- const offscript = new Offscript({
21
- apiUrl: "https://api.offscript.example",
22
- platformId: "platform-id",
23
- appVersion: "1.4.2",
24
- recording: { rrweb: true, actions: true, http: true, console: true, errors: true },
25
- privacy: { maskSensitiveInputs: true, captureRequestBodies: false, captureResponseBodies: false },
26
- });
27
-
28
- await offscript.init(); // retoma sessão persistida (reload / MPA), se houver
29
- await offscript.startRecording({
30
- testCaseId: "test-case-id",
31
- executionId: "execution-id",
32
- // sessionToken: "short-lived-session-token", // opcional: pode vir de ?offscript_token=
8
+ const sdk = new Offscript({
9
+ endpoint: "https://painel.example/api/recordings",
10
+ recording: { rrweb: true, actions: true, http: true, errors: true, iframes: true },
33
11
  });
34
- await offscript.pauseRecording(); // retomar gera um novo snapshot completo
35
- await offscript.resumeRecording();
36
- const summary = await offscript.stopRecording(); // { flushed, pendingEvents, completed }
37
- await offscript.destroy();
12
+ await sdk.init();
13
+ await sdk.startRecording();
14
+ const result = await sdk.complete();
38
15
  ```
39
16
 
40
- Todos os métodos são **idempotentes** (`init`, `startRecording` com a mesma execução, `stopRecording`, `pause/resume`, `destroy`). Chamadas concorrentes de `startRecording` compartilham a mesma promise; iniciar outra execução com uma ativa lança `InvalidStateError`.
17
+ `complete()` para os recorders e faz um único POST JSON com `Authorization: Bearer <token>`. O host configura o endpoint; o SDK obtém o token da URL ou da sessão da aba. Também e possível passar `endpoint` e `token` explícitamente a `complete` para integrações personalizadas.
41
18
 
42
- ### Estados e eventos
43
- `idle → initializing → recording ⇄ paused → stopping → stopped` (e `error`). `offscript.getState()` devolve `{ status, session, pending, lastError }`.
19
+ O payload contém `recording` (envelopes rrweb, ações, console, erros, contexto, iframes e ciclo de vida), `request` (envelopes HTTP) e `regression` (contrato Playwright com condições iniciais e passos). Não ha transporte durante a captura nem reenvio automático. Chamadas concorrentes de `complete` compartilham o envio; após sucesso, chamadas repetidas retornam o resultado. Após falha, os dados continuam disponíveis para uma nova chamada explícita.
44
20
 
45
- ```ts
46
- offscript.on("statusChange", ({ from, to }) => …);
47
- offscript.on("error", (e) => …); // OffscriptError tipado (e.code)
48
- offscript.on("sessionStart" | "sessionEnd" | "flush" | "overflow" | "disabled", …);
49
- ```
50
- Erros tipados: `ConfigError`, `AuthError`, `SessionExpiredError`, `SessionDisabledError`, `InvalidStateError`, `TransportError` (todos estendem `OffscriptError` com `code`).
51
-
52
- ### Autorização
53
- A gravação só começa depois que `POST /sdk/sessions` autoriza o token e responde `recording.enabled: true`. O token pode ser informado em `startRecording` ou lido automaticamente de `offscript_token` na query string. Depois de capturado, esse parâmetro é removido da barra de endereço; o token é guardado só em `sessionStorage` (escopo da aba) e usado em `Authorization: Bearer` em todas as chamadas, inclusive na finalização. Renovações chegam em `sessionToken` nas respostas. O backend pode desativar remotamente via heartbeat.
54
-
55
- ## Configuração
21
+ `stopRecording()` encerra localmente e devolve o payload sem publicar. `pauseRecording()` e `resumeRecording()` controlam a captura; retomar gera um snapshot completo. `discardRecording()` limpa os dados e volta ao estado inicial. `destroy()` libera a instrumentação sem enviar dados.
56
22
 
57
- | Opção | Padrão | Descrição |
58
- |---|---|---|
59
- | `apiUrl` | — | HTTPS obrigatório (HTTP só em localhost) |
60
- | `platformId` | — | Plataforma do SDK |
61
- | `appVersion` | — | Versão/build da app, enviada no contexto |
62
- | `autoResume` | `true` | Retoma sessão persistida em `init()` |
63
- | `correlationWindowMs` | `3000` | Janela para ligar http/erros à ação anterior |
64
- | `heartbeatIntervalMs` | `30000` | Usado se o backend não informar |
65
- | `recording.rrweb/actions/http/console/errors/context/iframes` | `true` | Liga cada módulo |
66
- | `recording.mouseMove` | `false` | Trajetória do mouse nas *ações* (rrweb já grava o movimento) |
67
- | `recording.consoleLevels` | todos | `log`, `info`, `warn`, `error` |
68
- | `recording.throttleMs` | `100` | Amostragem de mousemove |
69
- | `recording.rrwebCheckoutEveryMs` | `300000` | Snapshot completo periódico (0 desliga) |
70
- | `recording.rrwebRecordCanvas` | `false` | Canvas (custoso) |
71
- | `recording.testIdAttributes` | `data-testid, data-test, data-cy, data-qa` | Atributos de teste para seletores |
72
- | `recording.stableAttributes` | `name, data-id, aria-label` | Atributos estáveis extras |
73
- | `privacy.maskSensitiveInputs` | `false` | Máscara de inputs no replay |
74
- | `privacy.maskAllText` | `false` | Máscara de todo texto no replay |
75
- | `privacy.captureRequestBodies / captureResponseBodies` | `false` | Bodies HTTP |
76
- | `privacy.allowedInputSelectors` | `[data-offscript-allow]` | Campos cujo valor pode ser gravado |
77
- | `privacy.blockSelectors / maskTextSelectors / ignoreSelectors` | `[data-offscript-block]`… | Bloquear, mascarar texto, ignorar ações |
78
- | `privacy.ignoreUrls` | `[]` | Requisições não capturadas (string ou RegExp) |
79
- | `privacy.redactHeaders / redactKeys` | `[]` | Extras além do padrão |
80
- | `privacy.maxBodyBytes` | `16384` | Limite de body capturado |
81
- | `privacy.localRetentionMs` | `86400000` | Retenção local de eventos não enviados |
82
- | `transport.*` | ver `DEFAULT_TRANSPORT` | lote, flush, memória, retries, `compress` (gzip; requer suporte no backend), `persist` (IndexedDB) |
23
+ Com `controls` habilitado (padrão), o overlay aparece em `init()` somente quando existe um token de execução na URL ou na sessão da aba. Iniciar grava localmente; Finalizar envia a gravação ao endpoint configurado usando o token do SDK. `onComplete` continua disponível para integrações que precisem substituir esse comportamento.
83
24
 
84
- ### Privacidade (padrão seguro)
85
- Senhas, tokens, `Authorization`, cookies, chaves de API, JWT, CPF/CNPJ e cartões (Luhn) são redigidos **antes** de persistir ou enviar — em headers, query, bodies, console, erros, URLs/atributos/textos do rrweb. Valores digitados não vão para as ações (viram `valueRef`) a não ser em campos com `data-offscript-allow`. Use `data-offscript-block` / `.offscript-block` para remover um elemento do replay e `data-offscript-ignore` para ignorar suas ações. Isso ajuda a atender a LGPD, mas **não a substitui**: a base legal, o aviso aos testadores, a retenção e a exclusão no servidor são responsabilidade da Offscript/cliente.
25
+ As opções `recording` e `privacy` estão tipadas em `src/types/config.ts`. `captureSensitiveValues` vem desabilitada; habilitá-la inclui valores sensíveis na captura. Configure máscaras, seletores bloqueados e URLs ignoradas conforme a aplicação.
86
26
 
87
- ## Adaptadores
88
- `@offscript/sdk/vue` (Vue 2/3, sem importar Vue), `@offscript/sdk/nuxt` (client-only), `@offscript/sdk/vanilla` (`createOffscript`, `startOffscript`). Exemplos em [examples/](./examples).
27
+ Adaptadores disponíveis: `@offscript/sdk/vanilla`, `/vue` e `/nuxt`. O módulo `/playwright` exporta geração e validação do contrato; não executa o navegador. Iframes de outra origem fornecem apenas metadados de presença, foco e dimensões.
89
28
 
90
- ## Playwright
91
- `offscript.exportPlaywrightData()` ou `buildPlaywrightContract()` (de `@offscript/sdk/playwright`) — ver [docs/playwright-contract.md](./docs/playwright-contract.md). O SDK não executa Playwright.
29
+ Validação: `npm run typecheck`, `npm test`, `npm run build`, `npm run test:e2e` no pacote. Os testes de navegador exigem Chromium e suas bibliotecas de sistema.
92
30
 
93
- ## Documentação
94
- - [Arquitetura e decisões](./docs/architecture.md)
95
- - [Contrato da API](./docs/api-contract.md) — **o backend da Offscript precisa implementá-lo; este repositório só traz um servidor de referência de testes**
96
- - [Contrato de eventos](./docs/event-contract.md)
97
- - [Contrato Playwright](./docs/playwright-contract.md)
31
+ ## Continuidade entre páginas
98
32
 
99
- ## Limitações conhecidas
100
- - **Iframes cross-origin (ex.: seats.io)**: nem o SDK nem o rrweb acessam o conteúdo interno. São registrados presença (`origin` + `path`, sem query), dimensões, posição, foco (quando observável) e interações visíveis na página principal. Caminhos futuros: instrumentação/eventos públicos do seats.io, ou captura complementar via Playwright em ambiente de teste autorizado — **não implementados**.
101
- - **HTTP**: só `fetch` e `XMLHttpRequest` da página principal. Não captura WebSocket, requisições de iframes cross-origin, Service Workers, cookies HttpOnly, headers protegidos, nem `fetch` chamado antes do SDK iniciar. `duration` do fetch é até os headers. Bodies de streaming/binário são omitidos. Bodies de request do tipo `Request`/stream não são lidos.
102
- - **Console**: o stack aponta para o wrapper; arquivo/linha de `console.*` não é o da chamada original no DevTools.
103
- - **Entrega ao fechar a aba**: usa `fetch keepalive` (≤ 60 KB) sem ACK; o que não chegar é reenviado na próxima página da mesma aba. Se o navegador for encerrado abruptamente (crash, fechamento da última aba), **não há garantia de entrega** — eventos pendentes ficam no IndexedDB até a próxima sessão da aba/origem ou expirarem.
104
- - **Multi-aba**: gravação por aba (cada uma com `tabId` e sequência próprios). A detecção de aba clonada exige `navigator.locks`.
105
- - **Replay**: só reproduz o DOM gravado (iframe sandbox sem scripts). Canvas é opcional e custoso; vídeo/áudio e Shadow DOM fechado seguem as limitações do rrweb. `mousemove` sofre amostragem.
106
- - **Backpressure**: com a fila cheia, eventos de baixa prioridade são descartados (`overflow`) e o SDK força novo snapshot rrweb.
107
- - O `exportPlaywrightData()` do navegador cobre as ações da página atual (memória, até 5000); para a sessão inteira use `buildPlaywrightContract` com os eventos armazenados no backend.
108
- - **Não verificado**: execução em aplicações Vue/Nuxt/Laravel/Inertia reais, navegadores além do Chromium, Safari/iOS (limites de `keepalive`/IndexedDB), carga em páginas muito grandes (só há métricas internas `getMetrics()`, sem benchmark).
109
-
110
- ## Desenvolvimento
111
-
112
- ```bash
113
- npm install
114
- npm run typecheck
115
- npm test # 69 testes (vitest + jsdom + servidor HTTP de referência real)
116
- npm run test:e2e # Playwright em Chromium real (npx playwright install chromium)
117
- npm run build # tsup (ESM+CJS) + tsc (declarações)
118
- ```
33
+ Em sessões iniciadas com token, o SDK cria e mantém automaticamente uma chave opaca de rascunho em `sessionStorage`. Com ela, `init()` restaura o rascunho e retoma a captura quando ela estava ativa, preserva a pausa ou disponibiliza a gravação encerrada para envio. `persistenceKey` continua opcional para integrações que queiram controlar essa chave.
119
34
 
120
- ### Testes
121
- | Suite | Cobertura |
122
- |---|---|
123
- | `tests/session` | init, token inválido, desativação remota, idempotência, pausa/retomada, reload, sessão expirada, heartbeat, 401 no envio |
124
- | `tests/transport` | lotes ordenados, ACK, falha 5xx + backoff, persistência/recuperação, idempotência, 413, backpressure, flush final, offline/online, gzip |
125
- | `tests/rrweb` | snapshot, mutações, máscara de inputs, redação de URLs, pausa+snapshot, **replay** com `Replayer` |
126
- | `tests/actions` | click, input, select, checkbox, teclado, scroll, hover, drag, SPA, seletores |
127
- | `tests/http` | fetch ok/erro/abort/concorrente/grande/binário, XHR, sanitização, exclusão da própria API, correlação, remoção segura |
128
- | `tests/observability` | console, recursão, erros globais, correlação, contexto, iframe seats.io |
129
- | `tests/playwright` | normalização, contrato, validação, SelectorGenerator |
130
- | `tests/compat` | MPA com reload, SPA/Inertia, multi-aba, adaptadores Vue/Nuxt/vanilla, falha de recorder, destroy |
131
- | `e2e/mpa.spec.ts` | **Chromium real**: gravação com navegação MPA + reload + SPA, privacidade, correlação, contrato, replay completo no player rrweb |
35
+ Os eventos ficam no IndexedDB da origem. No descarregamento do documento, os últimos eventos ainda não confirmados são guardados em `sessionStorage` para a próxima pagina. `complete()` limpa o rascunho após HTTP de sucesso; `discardRecording()` também o remove. `destroy()` fecha o armazenamento e libera a instrumentação, preservando o rascunho local.
132
36
 
133
- ### Publicação
134
- Versionamento semântico (`MAJOR.MINOR.PATCH`); mudanças incompatíveis nos contratos de evento/Playwright incrementam o `schemaVersion`/`contractVersion` major. Fluxo: `npm version <patch|minor|major>` → `npm run verify` → `npm publish --access public` (o `prepublishOnly` já roda typecheck, testes e build).
37
+ O SDK lê o token de `offscript_token`, mantém a query na URL e guarda a credencial na sessão da aba para continuar após navegações. Após envio bem-sucedido, o SDK limpa o rascunho e não reabre o overlay nessa sessão; falhas mantém os dados para nova tentativa. A continuidade exige a mesma origem e o SDK inicializado em cada documento; armazenamento de navegador não é compartilhado entre domínios ou portas diferentes.
@@ -1,12 +1,12 @@
1
1
  'use strict';
2
2
 
3
- var chunkI6JPZAQA_cjs = require('../chunk-I6JPZAQA.cjs');
4
- require('../chunk-KQVK5EJR.cjs');
3
+ var chunkPCJRHW66_cjs = require('../chunk-PCJRHW66.cjs');
4
+ require('../chunk-MZXF2LF2.cjs');
5
5
 
6
6
  // src/adapters/nuxt.ts
7
7
  function createNuxtOffscript(config) {
8
8
  if (typeof window === "undefined") return null;
9
- const sdk = new chunkI6JPZAQA_cjs.Offscript(config);
9
+ const sdk = new chunkPCJRHW66_cjs.Offscript(config);
10
10
  void sdk.init().catch(() => void 0);
11
11
  return sdk;
12
12
  }
@@ -1,5 +1,5 @@
1
- import { Offscript } from '../chunk-IBEPWI7S.js';
2
- import '../chunk-WUFW7TEL.js';
1
+ import { Offscript } from '../chunk-5ZBR6FI5.js';
2
+ import '../chunk-KHKIWCLM.js';
3
3
 
4
4
  // src/adapters/nuxt.ts
5
5
  function createNuxtOffscript(config) {
@@ -1,18 +1,18 @@
1
1
  'use strict';
2
2
 
3
- var chunkTBPD2LMI_cjs = require('../chunk-TBPD2LMI.cjs');
4
- require('../chunk-I6JPZAQA.cjs');
5
- require('../chunk-KQVK5EJR.cjs');
3
+ var chunkRAUSXUGS_cjs = require('../chunk-RAUSXUGS.cjs');
4
+ require('../chunk-PCJRHW66.cjs');
5
+ require('../chunk-MZXF2LF2.cjs');
6
6
 
7
7
 
8
8
 
9
9
  Object.defineProperty(exports, "createOffscript", {
10
10
  enumerable: true,
11
- get: function () { return chunkTBPD2LMI_cjs.createOffscript; }
11
+ get: function () { return chunkRAUSXUGS_cjs.createOffscript; }
12
12
  });
13
13
  Object.defineProperty(exports, "startOffscript", {
14
14
  enumerable: true,
15
- get: function () { return chunkTBPD2LMI_cjs.startOffscript; }
15
+ get: function () { return chunkRAUSXUGS_cjs.startOffscript; }
16
16
  });
17
17
  //# sourceMappingURL=vanilla.cjs.map
18
18
  //# sourceMappingURL=vanilla.cjs.map
@@ -1,6 +1,5 @@
1
1
  import { Offscript } from "../core/OffscriptSDK";
2
2
  import type { OffscriptConfig } from "../types/config";
3
- import type { StartRecordingOptions } from "../types/session";
4
- /** Atalho para aplicações JS tradicionais, Blade e MPA: inicializa e retoma sessões persistidas. */
3
+ /** Atalho para aplicações JS tradicionais, Blade e MPA: inicializa a captura local. */
5
4
  export declare function createOffscript(config: OffscriptConfig): Promise<Offscript>;
6
- export declare function startOffscript(config: OffscriptConfig, start: StartRecordingOptions): Promise<Offscript>;
5
+ export declare function startOffscript(config: OffscriptConfig): Promise<Offscript>;
@@ -1,5 +1,5 @@
1
- export { createOffscript, startOffscript } from '../chunk-HECPMGDC.js';
2
- import '../chunk-IBEPWI7S.js';
3
- import '../chunk-WUFW7TEL.js';
1
+ export { createOffscript, startOffscript } from '../chunk-R2PPPR4K.js';
2
+ import '../chunk-5ZBR6FI5.js';
3
+ import '../chunk-KHKIWCLM.js';
4
4
  //# sourceMappingURL=vanilla.js.map
5
5
  //# sourceMappingURL=vanilla.js.map
@@ -1,13 +1,13 @@
1
1
  'use strict';
2
2
 
3
- var chunkI6JPZAQA_cjs = require('../chunk-I6JPZAQA.cjs');
4
- require('../chunk-KQVK5EJR.cjs');
3
+ var chunkPCJRHW66_cjs = require('../chunk-PCJRHW66.cjs');
4
+ require('../chunk-MZXF2LF2.cjs');
5
5
 
6
6
  // src/adapters/vue.ts
7
7
  var OFFSCRIPT_KEY = "$offscript";
8
8
  var OffscriptPlugin = {
9
9
  install(app, config) {
10
- const sdk = new chunkI6JPZAQA_cjs.Offscript(config);
10
+ const sdk = new chunkPCJRHW66_cjs.Offscript(config);
11
11
  void sdk.init().catch(() => void 0);
12
12
  if (app.config?.globalProperties) {
13
13
  app.config.globalProperties[OFFSCRIPT_KEY] = sdk;
@@ -1,5 +1,5 @@
1
- import { Offscript } from '../chunk-IBEPWI7S.js';
2
- import '../chunk-WUFW7TEL.js';
1
+ import { Offscript } from '../chunk-5ZBR6FI5.js';
2
+ import '../chunk-KHKIWCLM.js';
3
3
 
4
4
  // src/adapters/vue.ts
5
5
  var OFFSCRIPT_KEY = "$offscript";