@miranda0808/maya-codex 0.1.0 → 0.1.2

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.
@@ -1,324 +1,405 @@
1
- param(
2
- [Parameter(Mandatory = $true)]
3
- [ValidateSet('accounts', 'campaigns', 'adsets', 'ads', 'audiences')]
4
- [string]$Resource,
5
-
6
- [Parameter(Mandatory = $true)]
7
- [string]$Action,
8
-
9
- [string]$AccountId,
10
- [string]$Id,
11
- [string]$AdsetId,
12
- [string]$DatePreset = 'last_30d',
13
- [switch]$DryRun
14
- )
15
-
16
- $ErrorActionPreference = 'Stop'
17
-
18
- $allowedActions = @{
19
- accounts = @('list')
20
- campaigns = @('list', 'insights')
21
- adsets = @('list')
22
- ads = @('list')
23
- audiences = @('list')
24
- }
25
-
26
- if (-not $allowedActions.ContainsKey($Resource) -or $Action -notin $allowedActions[$Resource]) {
27
- throw "Unsupported operation '$Resource $Action'. Allowed read-only operations: accounts list, campaigns list|insights, adsets list, ads list, audiences list."
28
- }
29
-
30
- if (-not $env:META_ACCESS_TOKEN) {
31
- throw 'META_ACCESS_TOKEN environment variable is required.'
32
- }
33
-
34
- $effectiveAccountId = if ($AccountId) { $AccountId } else { $env:META_AD_ACCOUNT_ID }
35
-
36
- switch ("$Resource/$Action") {
37
- 'campaigns/list' {
38
- if (-not $effectiveAccountId) { throw 'META_AD_ACCOUNT_ID or -AccountId is required for campaigns list.' }
39
- }
40
- 'campaigns/insights' {
41
- if (-not $Id) { throw '-Id is required for campaigns insights.' }
42
- }
43
- 'adsets/list' {
44
- if (-not $effectiveAccountId) { throw 'META_AD_ACCOUNT_ID or -AccountId is required for adsets list.' }
45
- }
46
- 'ads/list' {
47
- if (-not $AdsetId) { throw '-AdsetId is required for ads list.' }
48
- }
49
- 'audiences/list' {
50
- if (-not $effectiveAccountId) { throw 'META_AD_ACCOUNT_ID or -AccountId is required for audiences list.' }
51
- }
52
- }
53
-
54
- function Get-SafeAccountAlias([string]$Value) {
55
- if (-not $Value) { return 'none' }
56
- $trimmed = $Value.Trim()
57
- if ($trimmed.Length -le 4) { return "acct-$trimmed" }
58
- return "acct-***$($trimmed.Substring($trimmed.Length - 4))"
59
- }
60
-
61
- function Sanitize-Data($InputObject) {
62
- if ($null -eq $InputObject) { return $null }
63
-
64
- if ($InputObject -is [System.Collections.IDictionary]) {
65
- $result = [ordered]@{}
66
- foreach ($key in $InputObject.Keys) {
67
- $keyText = [string]$key
68
- $value = $InputObject[$key]
69
- if ($keyText -match '(?i)authorization|access_token|token|secret|password|api[_-]?key') {
70
- $result[$keyText] = '***'
71
- } else {
72
- $result[$keyText] = Sanitize-Data $value
73
- }
74
- }
75
- return $result
76
- }
77
-
78
- if ($InputObject -is [pscustomobject]) {
79
- $result = [ordered]@{}
80
- foreach ($property in $InputObject.PSObject.Properties) {
81
- $name = [string]$property.Name
82
- $value = $property.Value
83
- if ($name -match '(?i)authorization|access_token|token|secret|password|api[_-]?key') {
84
- $result[$name] = '***'
85
- } else {
86
- $result[$name] = Sanitize-Data $value
87
- }
88
- }
89
- return $result
90
- }
91
-
92
- if ($InputObject -is [System.Collections.IEnumerable] -and -not ($InputObject -is [string])) {
93
- $items = @()
94
- foreach ($item in $InputObject) {
95
- $items += ,(Sanitize-Data $item)
96
- }
97
- return $items
98
- }
99
-
100
- if ($InputObject -is [string] -and $InputObject -eq $env:META_ACCESS_TOKEN) {
101
- return '***'
102
- }
103
-
104
- return $InputObject
105
- }
106
-
107
- function Get-JsonKey($Object) {
108
- return (($Object | ConvertTo-Json -Depth 20 -Compress) -replace '\s+', '')
109
- }
110
-
111
- function Get-RequestPolicy([string]$ResourceName, [string]$ActionName, [string]$Preset) {
112
- if ($ResourceName -eq 'campaigns' -and $ActionName -eq 'insights') {
113
- $safeClosedPresets = @('yesterday')
114
- if ($Preset -notin $safeClosedPresets) {
115
- return [ordered]@{
116
- cache_bypass = $true
117
- coverage_type = 'intraday'
118
- is_complete = $false
119
- ttl_minutes = 0
120
- }
121
- }
122
-
123
- return [ordered]@{
124
- cache_bypass = $false
125
- coverage_type = 'complete_day'
126
- is_complete = $true
127
- ttl_minutes = 360
128
- }
129
- }
130
-
131
- return [ordered]@{
132
- cache_bypass = $false
133
- coverage_type = 'complete_snapshot'
134
- is_complete = $true
135
- ttl_minutes = 15
136
- }
137
- }
138
-
139
- $repoRoot = Split-Path -Parent (Split-Path -Parent $PSScriptRoot)
140
- $cliPath = Join-Path $repoRoot 'tools\clis\meta-ads.js'
141
- $cacheRoot = Join-Path $repoRoot 'outputs\meta-cache'
142
-
143
- if (-not (Test-Path $cliPath)) {
144
- throw "Vendored CLI not found at $cliPath"
145
- }
146
-
147
- $normalizedParams = [ordered]@{
148
- resource = $Resource
149
- action = $Action
150
- account_id = if ($effectiveAccountId) { Get-SafeAccountAlias $effectiveAccountId } else { $null }
151
- id = if ($Id) { $Id } else { '' }
152
- adset_id = if ($AdsetId) { Get-SafeAccountAlias $AdsetId } else { $null }
153
- date_preset = if ($Resource -eq 'campaigns' -and $Action -eq 'insights') { $DatePreset } else { $null }
154
- }
155
- $requestKey = Get-JsonKey $normalizedParams
156
- $policy = Get-RequestPolicy -ResourceName $Resource -ActionName $Action -Preset $DatePreset
157
-
158
- function Find-FreshCacheEntry {
159
- param(
160
- [string]$CacheDirectory,
161
- [string]$WantedKey
162
- )
163
-
164
- if (-not (Test-Path $CacheDirectory)) {
165
- return $null
166
- }
167
-
168
- $now = Get-Date
169
- foreach ($file in Get-ChildItem $CacheDirectory -Filter '*.json' -File -ErrorAction SilentlyContinue) {
170
- try {
171
- $doc = Get-Content $file.FullName -Raw | ConvertFrom-Json
172
- }
173
- catch {
174
- continue
175
- }
176
-
177
- if (-not $doc.normalized_params) {
178
- continue
179
- }
180
-
181
- $docKey = Get-JsonKey (Sanitize-Data $doc.normalized_params)
182
- if ($docKey -ne $WantedKey) {
183
- continue
184
- }
185
-
186
- if (-not $doc.expires_at) {
187
- continue
188
- }
189
-
190
- try {
191
- $expiresAt = [datetimeoffset]::Parse([string]$doc.expires_at)
192
- }
193
- catch {
194
- continue
195
- }
196
-
197
- if ($expiresAt -le [datetimeoffset]$now) {
198
- continue
199
- }
200
-
201
- return [ordered]@{
202
- file = $file.FullName
203
- document = $doc
204
- }
205
- }
206
-
207
- return $null
208
- }
209
-
210
- function Invoke-MetaCli {
211
- param([switch]$UseDryRun)
212
-
213
- $cliArgs = @($cliPath, $Resource, $Action)
214
- if ($effectiveAccountId -and $Resource -in @('campaigns', 'adsets', 'audiences')) {
215
- $cliArgs += @('--account-id', $effectiveAccountId)
216
- }
217
- if ($Id) {
218
- $cliArgs += @('--id', $Id)
219
- }
220
- if ($AdsetId) {
221
- $cliArgs += @('--adset-id', $AdsetId)
222
- }
223
- if ($Resource -eq 'campaigns' -and $Action -eq 'insights' -and $DatePreset) {
224
- $cliArgs += @('--date-preset', $DatePreset)
225
- }
226
- if ($UseDryRun) {
227
- $cliArgs += '--dry-run'
228
- }
229
-
230
- $rawOutput = & node @cliArgs 2>&1
231
- $exitCode = $LASTEXITCODE
232
- $textOutput = ($rawOutput | Out-String).Trim()
233
-
234
- if ($exitCode -ne 0) {
235
- $safeError = $textOutput -replace [regex]::Escape($env:META_ACCESS_TOKEN), '***'
236
- throw "meta-ads.js failed: $safeError"
237
- }
238
-
239
- try {
240
- return $textOutput | ConvertFrom-Json
241
- }
242
- catch {
243
- throw "meta-ads.js returned non-JSON output: $textOutput"
244
- }
245
- }
246
-
247
- function Build-Result {
248
- param(
249
- [string]$CacheStatus,
250
- [string]$CoverageType,
251
- [bool]$IsComplete,
252
- [object]$Payload,
253
- [string]$AsOf,
254
- [string]$ExpiresAt,
255
- [string]$CachePath
256
- )
257
-
258
- $result = [ordered]@{
259
- source = 'meta-ads'
260
- dry_run = [bool]$DryRun
261
- cache_status = $CacheStatus
262
- safe_account_alias = Get-SafeAccountAlias $effectiveAccountId
263
- as_of = $AsOf
264
- expires_at = $ExpiresAt
265
- coverage_type = $CoverageType
266
- is_complete = $IsComplete
267
- normalized_params = $normalizedParams
268
- payload = $Payload
269
- forbidden_fields = @('authorization', 'access_token', 'token', 'secret', 'password', 'api_key')
270
- }
271
-
272
- if ($CachePath) {
273
- $result['cache_path'] = $CachePath
274
- }
275
-
276
- return $result
277
- }
278
-
279
- $cacheEntry = $null
280
- if (-not $policy.cache_bypass) {
281
- $cacheEntry = Find-FreshCacheEntry -CacheDirectory $cacheRoot -WantedKey $requestKey
282
- }
283
-
284
- if ($cacheEntry) {
285
- $doc = $cacheEntry.document
286
- $payload = Sanitize-Data $doc.payload
287
- $result = Build-Result -CacheStatus 'hit' -CoverageType ([string]$doc.coverage_type) -IsComplete ([bool]$doc.is_complete) -Payload $payload -AsOf ([string]$doc.as_of) -ExpiresAt ([string]$doc.expires_at) -CachePath $cacheEntry.file
288
- ($result | ConvertTo-Json -Depth 20)
289
- exit 0
290
- }
291
-
292
- $livePayload = Sanitize-Data (Invoke-MetaCli -UseDryRun:$DryRun)
293
- $cacheStatus = if ($policy.cache_bypass) { 'bypass' } else { 'miss' }
294
- $asOf = (Get-Date).ToString('o')
295
- $expiresAt = if ($policy.ttl_minutes -gt 0) { (Get-Date).AddMinutes($policy.ttl_minutes).ToString('o') } else { $null }
296
- $cachePath = $null
297
-
298
- if (-not $DryRun -and -not $policy.cache_bypass) {
299
- New-Item -ItemType Directory -Force -Path $cacheRoot | Out-Null
300
- $stamp = Get-Date -Format 'yyyyMMdd-HHmmss'
301
- $fileName = "meta-$Resource-$Action-$stamp.json"
302
- $cachePath = Join-Path $cacheRoot $fileName
303
-
304
- $cacheDocument = [ordered]@{
305
- schema_version = 1
306
- source = 'meta-ads'
307
- cache_status = $cacheStatus
308
- safe_account_alias = Get-SafeAccountAlias $effectiveAccountId
309
- as_of = $asOf
310
- fetched_at = $asOf
311
- expires_at = $expiresAt
312
- coverage_type = $policy.coverage_type
313
- is_complete = $policy.is_complete
314
- date_range = if ($Resource -eq 'campaigns' -and $Action -eq 'insights') { @{ date_preset = $DatePreset } } else { $null }
315
- normalized_params = $normalizedParams
316
- forbidden_fields = @('authorization', 'access_token', 'token', 'secret', 'password', 'api_key')
317
- payload = $livePayload
318
- }
319
-
320
- ($cacheDocument | ConvertTo-Json -Depth 20) | Set-Content $cachePath
321
- }
322
-
323
- $result = Build-Result -CacheStatus $cacheStatus -CoverageType $policy.coverage_type -IsComplete $policy.is_complete -Payload $livePayload -AsOf $asOf -ExpiresAt $expiresAt -CachePath $cachePath
1
+ param(
2
+ [Parameter(Mandatory = $true)]
3
+ [ValidateSet('accounts', 'campaigns', 'adsets', 'ads', 'audiences')]
4
+ [string]$Resource,
5
+
6
+ [Parameter(Mandatory = $true)]
7
+ [string]$Action,
8
+
9
+ [string]$AccountId,
10
+ [string]$Id,
11
+ [string]$AdsetId,
12
+ [string]$DatePreset = 'last_30d',
13
+ [switch]$DryRun
14
+ )
15
+
16
+ $ErrorActionPreference = 'Stop'
17
+
18
+ $allowedActions = @{
19
+ accounts = @('list')
20
+ campaigns = @('list', 'insights')
21
+ adsets = @('list')
22
+ ads = @('list')
23
+ audiences = @('list')
24
+ }
25
+
26
+ if (-not $allowedActions.ContainsKey($Resource) -or $Action -notin $allowedActions[$Resource]) {
27
+ throw "Unsupported operation '$Resource $Action'. Allowed read-only operations: accounts list, campaigns list|insights, adsets list, ads list, audiences list."
28
+ }
29
+
30
+ function Read-EnvFile([string]$Path) {
31
+ $values = [ordered]@{}
32
+
33
+ if (-not (Test-Path $Path)) {
34
+ return $values
35
+ }
36
+
37
+ $content = Get-Content $Path -Raw
38
+ $normalized = $content -replace "`r`n?", "`n"
39
+
40
+ foreach ($line in ($normalized -split "`n")) {
41
+ $trimmed = $line.Trim()
42
+ if (-not $trimmed -or $trimmed.StartsWith('#')) {
43
+ continue
44
+ }
45
+
46
+ $separatorIndex = $trimmed.IndexOf('=')
47
+ if ($separatorIndex -lt 1) {
48
+ continue
49
+ }
50
+
51
+ $key = $trimmed.Substring(0, $separatorIndex).Trim()
52
+ $value = $trimmed.Substring($separatorIndex + 1).Trim()
53
+
54
+ if (($value.StartsWith('"') -and $value.EndsWith('"')) -or ($value.StartsWith("'") -and $value.EndsWith("'"))) {
55
+ $value = $value.Substring(1, $value.Length - 2)
56
+ }
57
+
58
+ $values[$key] = $value
59
+ }
60
+
61
+ return $values
62
+ }
63
+
64
+ function Resolve-MetaEnv([string]$WorkspaceRoot) {
65
+ $metaKeys = @('META_ACCESS_TOKEN', 'META_AD_ACCOUNT_ID')
66
+ $mergedValues = [ordered]@{}
67
+
68
+ foreach ($envPath in @((Join-Path $WorkspaceRoot '.env'), (Join-Path $WorkspaceRoot '.env.local'))) {
69
+ $fileValues = Read-EnvFile $envPath
70
+ foreach ($metaKey in $metaKeys) {
71
+ if ($fileValues.Contains($metaKey)) {
72
+ $mergedValues[$metaKey] = [string]$fileValues[$metaKey]
73
+ }
74
+ }
75
+ }
76
+
77
+ return [ordered]@{
78
+ access_token = if (-not [string]::IsNullOrWhiteSpace($env:META_ACCESS_TOKEN)) {
79
+ $env:META_ACCESS_TOKEN
80
+ } elseif ($mergedValues.Contains('META_ACCESS_TOKEN')) {
81
+ [string]$mergedValues['META_ACCESS_TOKEN']
82
+ } else {
83
+ $null
84
+ }
85
+ ad_account_id = if (-not [string]::IsNullOrWhiteSpace($env:META_AD_ACCOUNT_ID)) {
86
+ $env:META_AD_ACCOUNT_ID
87
+ } elseif ($mergedValues.Contains('META_AD_ACCOUNT_ID')) {
88
+ [string]$mergedValues['META_AD_ACCOUNT_ID']
89
+ } else {
90
+ $null
91
+ }
92
+ }
93
+ }
94
+
95
+ $repoRoot = Split-Path -Parent (Split-Path -Parent $PSScriptRoot)
96
+ $metaConfig = Resolve-MetaEnv -WorkspaceRoot $repoRoot
97
+ $metaAccessToken = [string]$metaConfig.access_token
98
+ $defaultMetaAccountId = [string]$metaConfig.ad_account_id
99
+
100
+ if (-not $metaAccessToken) {
101
+ throw 'META_ACCESS_TOKEN environment variable is required.'
102
+ }
103
+
104
+ $effectiveAccountId = if ($AccountId) { $AccountId } else { $defaultMetaAccountId }
105
+
106
+ switch ("$Resource/$Action") {
107
+ 'campaigns/list' {
108
+ if (-not $effectiveAccountId) { throw 'META_AD_ACCOUNT_ID or -AccountId is required for campaigns list.' }
109
+ }
110
+ 'campaigns/insights' {
111
+ if (-not $Id) { throw '-Id is required for campaigns insights.' }
112
+ }
113
+ 'adsets/list' {
114
+ if (-not $effectiveAccountId) { throw 'META_AD_ACCOUNT_ID or -AccountId is required for adsets list.' }
115
+ }
116
+ 'ads/list' {
117
+ if (-not $AdsetId) { throw '-AdsetId is required for ads list.' }
118
+ }
119
+ 'audiences/list' {
120
+ if (-not $effectiveAccountId) { throw 'META_AD_ACCOUNT_ID or -AccountId is required for audiences list.' }
121
+ }
122
+ }
123
+
124
+ function Get-SafeAccountAlias([string]$Value) {
125
+ if (-not $Value) { return 'none' }
126
+ $trimmed = $Value.Trim()
127
+ if ($trimmed.Length -le 4) { return "acct-$trimmed" }
128
+ return "acct-***$($trimmed.Substring($trimmed.Length - 4))"
129
+ }
130
+
131
+ function Sanitize-Data($InputObject) {
132
+ if ($null -eq $InputObject) { return $null }
133
+
134
+ if ($InputObject -is [System.Collections.IDictionary]) {
135
+ $result = [ordered]@{}
136
+ foreach ($key in $InputObject.Keys) {
137
+ $keyText = [string]$key
138
+ $value = $InputObject[$key]
139
+ if ($keyText -match '(?i)authorization|access_token|token|secret|password|api[_-]?key') {
140
+ $result[$keyText] = '***'
141
+ } else {
142
+ $result[$keyText] = Sanitize-Data $value
143
+ }
144
+ }
145
+ return $result
146
+ }
147
+
148
+ if ($InputObject -is [pscustomobject]) {
149
+ $result = [ordered]@{}
150
+ foreach ($property in $InputObject.PSObject.Properties) {
151
+ $name = [string]$property.Name
152
+ $value = $property.Value
153
+ if ($name -match '(?i)authorization|access_token|token|secret|password|api[_-]?key') {
154
+ $result[$name] = '***'
155
+ } else {
156
+ $result[$name] = Sanitize-Data $value
157
+ }
158
+ }
159
+ return $result
160
+ }
161
+
162
+ if ($InputObject -is [System.Collections.IEnumerable] -and -not ($InputObject -is [string])) {
163
+ $items = @()
164
+ foreach ($item in $InputObject) {
165
+ $items += ,(Sanitize-Data $item)
166
+ }
167
+ return $items
168
+ }
169
+
170
+ if ($InputObject -is [string] -and $InputObject -eq $metaAccessToken) {
171
+ return '***'
172
+ }
173
+
174
+ return $InputObject
175
+ }
176
+
177
+ function Get-JsonKey($Object) {
178
+ return (($Object | ConvertTo-Json -Depth 20 -Compress) -replace '\s+', '')
179
+ }
180
+
181
+ function Get-RequestPolicy([string]$ResourceName, [string]$ActionName, [string]$Preset) {
182
+ if ($ResourceName -eq 'campaigns' -and $ActionName -eq 'insights') {
183
+ $safeClosedPresets = @('yesterday')
184
+ if ($Preset -notin $safeClosedPresets) {
185
+ return [ordered]@{
186
+ cache_bypass = $true
187
+ coverage_type = 'intraday'
188
+ is_complete = $false
189
+ ttl_minutes = 0
190
+ }
191
+ }
192
+
193
+ return [ordered]@{
194
+ cache_bypass = $false
195
+ coverage_type = 'complete_day'
196
+ is_complete = $true
197
+ ttl_minutes = 360
198
+ }
199
+ }
200
+
201
+ return [ordered]@{
202
+ cache_bypass = $false
203
+ coverage_type = 'complete_snapshot'
204
+ is_complete = $true
205
+ ttl_minutes = 15
206
+ }
207
+ }
208
+
209
+ $cliPath = Join-Path $repoRoot 'tools\clis\meta-ads.js'
210
+ $cacheRoot = Join-Path $repoRoot 'outputs\meta-cache'
211
+
212
+ if (-not (Test-Path $cliPath)) {
213
+ throw "Vendored CLI not found at $cliPath"
214
+ }
215
+
216
+ $normalizedParams = [ordered]@{
217
+ resource = $Resource
218
+ action = $Action
219
+ account_id = if ($effectiveAccountId) { Get-SafeAccountAlias $effectiveAccountId } else { $null }
220
+ id = if ($Id) { $Id } else { '' }
221
+ adset_id = if ($AdsetId) { Get-SafeAccountAlias $AdsetId } else { $null }
222
+ date_preset = if ($Resource -eq 'campaigns' -and $Action -eq 'insights') { $DatePreset } else { $null }
223
+ }
224
+ $requestKey = Get-JsonKey $normalizedParams
225
+ $policy = Get-RequestPolicy -ResourceName $Resource -ActionName $Action -Preset $DatePreset
226
+
227
+ function Find-FreshCacheEntry {
228
+ param(
229
+ [string]$CacheDirectory,
230
+ [string]$WantedKey
231
+ )
232
+
233
+ if (-not (Test-Path $CacheDirectory)) {
234
+ return $null
235
+ }
236
+
237
+ $now = Get-Date
238
+ foreach ($file in Get-ChildItem $CacheDirectory -Filter '*.json' -File -ErrorAction SilentlyContinue) {
239
+ try {
240
+ $doc = Get-Content $file.FullName -Raw | ConvertFrom-Json
241
+ }
242
+ catch {
243
+ continue
244
+ }
245
+
246
+ if (-not $doc.normalized_params) {
247
+ continue
248
+ }
249
+
250
+ $docKey = Get-JsonKey (Sanitize-Data $doc.normalized_params)
251
+ if ($docKey -ne $WantedKey) {
252
+ continue
253
+ }
254
+
255
+ if (-not $doc.expires_at) {
256
+ continue
257
+ }
258
+
259
+ try {
260
+ $expiresAt = [datetimeoffset]::Parse([string]$doc.expires_at)
261
+ }
262
+ catch {
263
+ continue
264
+ }
265
+
266
+ if ($expiresAt -le [datetimeoffset]$now) {
267
+ continue
268
+ }
269
+
270
+ return [ordered]@{
271
+ file = $file.FullName
272
+ document = $doc
273
+ }
274
+ }
275
+
276
+ return $null
277
+ }
278
+
279
+ function Invoke-MetaCli {
280
+ param([switch]$UseDryRun)
281
+
282
+ $cliArgs = @($cliPath, $Resource, $Action)
283
+ if ($effectiveAccountId -and $Resource -in @('campaigns', 'adsets', 'audiences')) {
284
+ $cliArgs += @('--account-id', $effectiveAccountId)
285
+ }
286
+ if ($Id) {
287
+ $cliArgs += @('--id', $Id)
288
+ }
289
+ if ($AdsetId) {
290
+ $cliArgs += @('--adset-id', $AdsetId)
291
+ }
292
+ if ($Resource -eq 'campaigns' -and $Action -eq 'insights' -and $DatePreset) {
293
+ $cliArgs += @('--date-preset', $DatePreset)
294
+ }
295
+ if ($UseDryRun) {
296
+ $cliArgs += '--dry-run'
297
+ }
298
+
299
+ $previousToken = [Environment]::GetEnvironmentVariable('META_ACCESS_TOKEN', 'Process')
300
+ $previousAccount = [Environment]::GetEnvironmentVariable('META_AD_ACCOUNT_ID', 'Process')
301
+
302
+ try {
303
+ [Environment]::SetEnvironmentVariable('META_ACCESS_TOKEN', $metaAccessToken, 'Process')
304
+ [Environment]::SetEnvironmentVariable('META_AD_ACCOUNT_ID', $defaultMetaAccountId, 'Process')
305
+
306
+ $rawOutput = & node @cliArgs 2>&1
307
+ $exitCode = $LASTEXITCODE
308
+ $textOutput = ($rawOutput | Out-String).Trim()
309
+
310
+ if ($exitCode -ne 0) {
311
+ $safeError = $textOutput -replace [regex]::Escape($metaAccessToken), '***'
312
+ throw "meta-ads.js failed: $safeError"
313
+ }
314
+
315
+ try {
316
+ return $textOutput | ConvertFrom-Json
317
+ }
318
+ catch {
319
+ throw "meta-ads.js returned non-JSON output: $textOutput"
320
+ }
321
+ }
322
+ finally {
323
+ [Environment]::SetEnvironmentVariable('META_ACCESS_TOKEN', $previousToken, 'Process')
324
+ [Environment]::SetEnvironmentVariable('META_AD_ACCOUNT_ID', $previousAccount, 'Process')
325
+ }
326
+ }
327
+
328
+ function Build-Result {
329
+ param(
330
+ [string]$CacheStatus,
331
+ [string]$CoverageType,
332
+ [bool]$IsComplete,
333
+ [object]$Payload,
334
+ [string]$AsOf,
335
+ [string]$ExpiresAt,
336
+ [string]$CachePath
337
+ )
338
+
339
+ $result = [ordered]@{
340
+ source = 'meta-ads'
341
+ dry_run = [bool]$DryRun
342
+ cache_status = $CacheStatus
343
+ safe_account_alias = Get-SafeAccountAlias $effectiveAccountId
344
+ as_of = $AsOf
345
+ expires_at = $ExpiresAt
346
+ coverage_type = $CoverageType
347
+ is_complete = $IsComplete
348
+ normalized_params = $normalizedParams
349
+ payload = $Payload
350
+ forbidden_fields = @('authorization', 'access_token', 'token', 'secret', 'password', 'api_key')
351
+ }
352
+
353
+ if ($CachePath) {
354
+ $result['cache_path'] = $CachePath
355
+ }
356
+
357
+ return $result
358
+ }
359
+
360
+ $cacheEntry = $null
361
+ if (-not $policy.cache_bypass) {
362
+ $cacheEntry = Find-FreshCacheEntry -CacheDirectory $cacheRoot -WantedKey $requestKey
363
+ }
364
+
365
+ if ($cacheEntry) {
366
+ $doc = $cacheEntry.document
367
+ $payload = Sanitize-Data $doc.payload
368
+ $result = Build-Result -CacheStatus 'hit' -CoverageType ([string]$doc.coverage_type) -IsComplete ([bool]$doc.is_complete) -Payload $payload -AsOf ([string]$doc.as_of) -ExpiresAt ([string]$doc.expires_at) -CachePath $cacheEntry.file
369
+ ($result | ConvertTo-Json -Depth 20)
370
+ exit 0
371
+ }
372
+
373
+ $livePayload = Sanitize-Data (Invoke-MetaCli -UseDryRun:$DryRun)
374
+ $cacheStatus = if ($policy.cache_bypass) { 'bypass' } else { 'miss' }
375
+ $asOf = (Get-Date).ToString('o')
376
+ $expiresAt = if ($policy.ttl_minutes -gt 0) { (Get-Date).AddMinutes($policy.ttl_minutes).ToString('o') } else { $null }
377
+ $cachePath = $null
378
+
379
+ if (-not $DryRun -and -not $policy.cache_bypass) {
380
+ New-Item -ItemType Directory -Force -Path $cacheRoot | Out-Null
381
+ $stamp = Get-Date -Format 'yyyyMMdd-HHmmss'
382
+ $fileName = "meta-$Resource-$Action-$stamp.json"
383
+ $cachePath = Join-Path $cacheRoot $fileName
384
+
385
+ $cacheDocument = [ordered]@{
386
+ schema_version = 1
387
+ source = 'meta-ads'
388
+ cache_status = $cacheStatus
389
+ safe_account_alias = Get-SafeAccountAlias $effectiveAccountId
390
+ as_of = $asOf
391
+ fetched_at = $asOf
392
+ expires_at = $expiresAt
393
+ coverage_type = $policy.coverage_type
394
+ is_complete = $policy.is_complete
395
+ date_range = if ($Resource -eq 'campaigns' -and $Action -eq 'insights') { @{ date_preset = $DatePreset } } else { $null }
396
+ normalized_params = $normalizedParams
397
+ forbidden_fields = @('authorization', 'access_token', 'token', 'secret', 'password', 'api_key')
398
+ payload = $livePayload
399
+ }
400
+
401
+ ($cacheDocument | ConvertTo-Json -Depth 20) | Set-Content $cachePath
402
+ }
403
+
404
+ $result = Build-Result -CacheStatus $cacheStatus -CoverageType $policy.coverage_type -IsComplete $policy.is_complete -Payload $livePayload -AsOf $asOf -ExpiresAt $expiresAt -CachePath $cachePath
324
405
  ($result | ConvertTo-Json -Depth 20)